Skip to content

Commit ac399cf

Browse files
committed
refactor(hub,hub-ui): dockPreferences as a createUi() option; DevframeHubUi.setup(ctx)
Dock-bar preferences (categoryOrder / maxVisibleItems / defaultMode / defaultPosition) are reference-UI rendering concerns, not per-devframe declarations — move them off `DevframeDefinition.dockPreferences` (removed from core, along with the `DevframeDockPreferences` type and the hub's `configs.dock` aggregation) into `createUi({ dockPreferences })`, published under `ConnectionMeta.configs.ui.dockPreferences` alongside `branding` and `embeddedVisibility`. Also replace the `DevframeHubUi.configs` producer with a `setup(ctx)` hook: the UI slot now publishes its config through the generic `ctx.staticConfig` (`createUi()`'s setup writes `ctx.staticConfig.ui = { branding, ... }`), so the hub's mount handler no longer special-cases a `ui` merge. Built with the help of an agent.
1 parent 6d3b3c8 commit ac399cf

18 files changed

Lines changed: 128 additions & 218 deletions

File tree

‎docs/guide/build-your-own-hub-ui.md‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface DevframeHubUi {
1616
viewer?: { distDir: string } // a standalone SPA served at the hub base
1717
embedded?: { entry: string } // a self-contained bootstrap at <base>embedded.js
1818
assets?: Record<string, () => string | Uint8Array> // extra UI-owned files
19-
configs?: () => Record<string, unknown> // static config, published as ConnectionMeta.configs.ui
19+
setup?: (ctx) => void | Promise<void> // publish static config via ctx.staticConfig
2020
}
2121
```
2222

@@ -25,14 +25,13 @@ prebuilt assets: the viewer SPA is built with relative asset paths, and the
2525
embedded entry is one self-contained ES module that mounts your dock into any
2626
host page.
2727

28-
`configs` publishes whatever you return verbatim as
29-
`ConnectionMeta.configs.ui` — the reference UI's `createUi({ branding })` uses
30-
it to deliver `{ branding }`, read by the dock from the one connection
31-
handshake it already performs, rather than a separate fetched file. The hub
32-
never interprets this object; it's a policy-free pass-through to your own
33-
client code. It's the read-only counterpart to `assets`: reach for `configs`
34-
for small, structured, boot-time config, and `assets` for arbitrary served
35-
files.
28+
`setup(ctx)` runs once during hub init — write your static, boot-time config
29+
to `ctx.staticConfig`, which is serialized into `ConnectionMeta.configs` and
30+
read by the client from the one connection handshake it already performs. The
31+
reference UI's `createUi({ branding })` uses it to set
32+
`ctx.staticConfig.ui = { branding, … }`; the hub never interprets what you
33+
write. It's the structured, read-only counterpart to `assets` (arbitrary
34+
served files).
3635

3736
## The client contracts
3837

‎docs/guide/hub-initiate.md‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,26 @@ interface DevframeHubUi {
5757
viewer?: { distDir: string } // a standalone SPA served at the namespace root
5858
embedded?: { entry: string } // a prebuilt bootstrap served at <base>embedded.js
5959
assets?: Record<string, () => string | Uint8Array> // extra UI-owned files
60-
configs?: () => Record<string, unknown> // static config, published as ConnectionMeta.configs.ui
60+
setup?: (ctx) => void | Promise<void> // publish static config via ctx.staticConfig
6161
}
6262
```
6363

64-
`@devframes/hub-ui`'s `createUi()` is the reference implementation: a standalone viewer plus the floating dock — one `<script type="module" src="/__devframes/embedded.js">` tag in the host page and the dock mounts itself. A viewer product supplies a different object to the same slot and reuses all the infrastructure.
64+
`@devframes/hub-ui`'s `createUi()` is the reference implementation: a standalone viewer plus the floating dock — one `<script type="module" src="/__devframes/embedded.js">` tag in the host page and the dock mounts itself. A viewer product supplies a different object to the same slot and reuses all the infrastructure. Its `setup(ctx)` publishes the reference UI's config to `ctx.staticConfig.ui`, which rides `ConnectionMeta.configs.ui` to the client.
6565

66-
`createUi()` takes a few options: `branding` (rebrand the reference UI — logo, product name, primary color) and `embeddedVisibility` for the floating dock's reveal policy:
66+
`createUi()` takes a few options:
67+
68+
- **`branding`** — rebrand the reference UI (logo, product name, primary color).
69+
- **`dockPreferences`** — dock-bar rendering: `categoryOrder`, floating-dock `maxVisibleItems`, and the first-run `defaultMode` (`'float'` / `'edge'`) and `defaultPosition`.
70+
- **`embeddedVisibility`** — the floating dock's reveal policy:
71+
- `'normal'` (default) — the dock is shown immediately.
72+
- `'passive'` — the dock starts hidden with a console hint; `Shift+Alt+D` reveals it, and the reveal persists per-origin so later sessions start shown.
73+
- `'hidden'` — the dock starts hidden; `Shift+Alt+D` reveals it for the current session only.
6774

6875
```ts
69-
createUi({ embeddedVisibility: 'passive' })
76+
createUi({ embeddedVisibility: 'passive', dockPreferences: { defaultMode: 'edge' } })
7077
```
7178

72-
- `'normal'` (default) — the dock is shown immediately.
73-
- `'passive'` — the dock starts hidden with a console hint; `Shift+Alt+D` reveals it, and the reveal persists per-origin so later sessions start shown.
74-
- `'hidden'` — the dock starts hidden; `Shift+Alt+D` reveals it for the current session only.
75-
76-
Both ride `ConnectionMeta.configs.ui` to the client. Like the float/edge dock mode, `embeddedVisibility` seeds a user-overridable preference — the visitor's own reveal/hide wins from then on.
79+
Each seeds a user-overridable preference — the config sets the default, the visitor's own choice (reveal/hide, float/edge, …) wins from then on.
7780

7881
## Renderer modules
7982

‎packages/devframe/src/types/devframe.ts‎

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -264,41 +264,6 @@ export interface DevframeDockDefaults {
264264
groupId?: string
265265
}
266266

267-
/**
268-
* A devframe's opinion about the **hub-wide** dock bar — not attributes of
269-
* its own synthesized entry, but preferences the hub aggregates across every
270-
* installed devframe and publishes once via `ConnectionMeta.configs.dock`.
271-
* Declared on {@link DevframeDefinition.dockPreferences}. Standalone adapters
272-
* (`cli` / `spa` / `build`) ignore this entirely.
273-
*/
274-
export interface DevframeDockPreferences {
275-
/**
276-
* The top-level dock-bar **category** ordering. Every installed devframe's
277-
* `categoryOrder` is shallow-merged into one aggregate (last-installed wins
278-
* per key) and merged beneath `DEFAULT_CATEGORIES_ORDER`. A host page's own
279-
* `createDevframeClientHost({ categoryOrder })` still overrides it.
280-
*/
281-
categoryOrder?: Record<string, number>
282-
/**
283-
* Preferred inline-item capacity for the floating dock bar before entries
284-
* overflow. The last installed devframe declaring it wins; an explicit
285-
* `layout` prop passed to the dock UI still overrides it. Edge mode ignores
286-
* this by design — it shows every entry with no capacity cutoff.
287-
*/
288-
maxVisibleItems?: number
289-
/**
290-
* Seeds a first-run visitor's dock mode. Only applies when the visitor has
291-
* no stored dock preference yet; never overwrites one who already moved
292-
* their dock. The last installed devframe declaring it wins.
293-
*/
294-
defaultMode?: 'float' | 'edge'
295-
/**
296-
* Seeds a first-run visitor's dock position, same override semantics as
297-
* {@link defaultMode}.
298-
*/
299-
defaultPosition?: 'left' | 'right' | 'top' | 'bottom'
300-
}
301-
302267
export interface DevframeSpaOptions {
303268
base?: string
304269
/**
@@ -348,15 +313,6 @@ export interface DevframeDefinition {
348313
* @see {@link DevframeDockDefaults}
349314
*/
350315
dock?: DevframeDockDefaults
351-
/**
352-
* This devframe's opinion about the hub-wide dock bar (category ordering,
353-
* float-mode capacity, first-run mode/position). Consulted only by the hub
354-
* install path, which aggregates it across every installed devframe into
355-
* `ConnectionMeta.configs.dock`; standalone adapters ignore it.
356-
*
357-
* @see {@link DevframeDockPreferences}
358-
*/
359-
dockPreferences?: DevframeDockPreferences
360316
/**
361317
* Mount path override. Defaults depend on the adapter:
362318
* `/` for standalone (`cli` / `spa` / `build`), `/__<id>/` for hosted
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* The reference UI's dock-bar rendering preferences, set via
3+
* `createUi({ dockPreferences })` and published as
4+
* `ConnectionMeta.configs.ui.dockPreferences`. Read by the embedded dock and
5+
* the standalone viewer at boot.
6+
*
7+
* Like the float/edge dock mode, these seed user-overridable state — the
8+
* config sets the default, the visitor's own choice wins from then on.
9+
*/
10+
export interface DevframeDockPreferences {
11+
/**
12+
* The top-level dock-bar **category** ordering — a map of category id →
13+
* ordering weight (lower sorts earlier), merged beneath
14+
* `DEFAULT_CATEGORIES_ORDER`.
15+
*/
16+
categoryOrder?: Record<string, number>
17+
/**
18+
* Preferred inline-item capacity for the floating dock bar before entries
19+
* overflow. Edge mode ignores it — it shows every entry with no cutoff.
20+
*/
21+
maxVisibleItems?: number
22+
/** Seeds a first-run visitor's dock mode (float vs edge). */
23+
defaultMode?: 'float' | 'edge'
24+
/** Seeds a first-run visitor's dock position. */
25+
defaultPosition?: 'left' | 'right' | 'top' | 'bottom'
26+
}

‎packages/hub-ui/src/client/embedded/index.ts‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,21 @@ async function mountDock(): Promise<void> {
4040
simpleAuth: false,
4141
})
4242

43-
// The hub's aggregated dock-bar preferences (declared by installed
44-
// devframes), delivered once via the connection handshake we just
45-
// performed — fixed for the life of this server, never re-fetched.
46-
const dockConfig = rpc.connectionMeta.configs?.dock
43+
// The reference UI's dock-bar preferences (`createUi({ dockPreferences })`),
44+
// delivered once via the connection handshake we just performed — fixed for
45+
// the life of this server, never re-fetched.
46+
const dockPreferences = rpc.connectionMeta.configs?.ui?.dockPreferences
4747

4848
const defaultStore = DEFAULT_DOCK_PANEL_STORE()
4949
const state = useLocalStorage<DockPanelStorage>(
5050
'devframes-dock-state',
5151
{
5252
...defaultStore,
53-
// Seed a first-run visitor's mode/position from the hub's declared
53+
// Seed a first-run visitor's mode/position from the configured
5454
// defaults — `useLocalStorage`'s own `mergeDefaults` already limits
5555
// this to a visitor with no stored preference yet.
56-
...(dockConfig?.defaultMode ? { mode: dockConfig.defaultMode } : {}),
57-
...(dockConfig?.defaultPosition ? { position: dockConfig.defaultPosition } : {}),
56+
...(dockPreferences?.defaultMode ? { mode: dockPreferences.defaultMode } : {}),
57+
...(dockPreferences?.defaultPosition ? { position: dockPreferences.defaultPosition } : {}),
5858
},
5959
{ mergeDefaults: true },
6060
)
@@ -71,7 +71,7 @@ async function mountDock(): Promise<void> {
7171
const { DockEmbedded } = await import('../components/DockEmbedded')
7272
dockEl = new DockEmbedded({
7373
context,
74-
...(dockConfig?.maxVisibleItems !== undefined ? { layout: { maxVisibleItems: dockConfig.maxVisibleItems } } : {}),
74+
...(dockPreferences?.maxVisibleItems !== undefined ? { layout: { maxVisibleItems: dockPreferences.maxVisibleItems } } : {}),
7575
}) as unknown as HTMLElement
7676
// Inline on the host element — beats the generated `:host` ramp defaults and
7777
// inherits through the shadow tree. The embedded bootstrap never touches the

‎packages/hub-ui/src/client/state/context.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,10 @@ export async function createDocksContext(
353353

354354
// Settings store, `settings`, and `getWhenContext` are established earlier
355355
// (right before `switchEntry`) — its group→member resolution needs them.
356-
// `categoryOrderOverride` folds in every installed devframe's own declared
357-
// `dock.categoryOrder`, aggregated hub-wide and delivered once via the
358-
// connection handshake (`ConnectionMeta.configs.dock.categoryOrder`).
359-
const categoryOrderOverride = rpc.connectionMeta.configs?.dock?.categoryOrder
356+
// `categoryOrderOverride` folds in the reference UI's configured
357+
// `dockPreferences.categoryOrder` (`createUi({ dockPreferences })`),
358+
// delivered once via the connection handshake.
359+
const categoryOrderOverride = rpc.connectionMeta.configs?.ui?.dockPreferences?.categoryOrder
360360
const groupedEntries = computed(() => {
361361
return docksGroupByCategories(entries.value, settings.value, { whenContext: getWhenContext(), collapseGroups: true, categoryOrderOverride })
362362
})

‎packages/hub-ui/src/index.ts‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import type { DevframeHubUi } from '@devframes/hub/initiate'
2+
import type { DevframeDockPreferences } from './client/dock-preferences'
23
import type { EmbeddedVisibility } from './client/embedded/visibility'
34
import type { DevframeBranding } from './client/state/branding'
45
import { existsSync } from 'node:fs'
56
import { join } from 'node:path'
67
import { fileURLToPath } from 'node:url'
78

9+
export type { DevframeDockPreferences } from './client/dock-preferences'
810
export type { EmbeddedVisibility } from './client/embedded/visibility'
911
export type { DevframeBranding } from './client/state/branding'
1012

1113
declare module 'devframe/types' {
1214
interface DevframeConnectionConfigsRegistry {
13-
ui: { branding?: DevframeBranding, embeddedVisibility?: EmbeddedVisibility }
15+
ui: {
16+
branding?: DevframeBranding
17+
embeddedVisibility?: EmbeddedVisibility
18+
dockPreferences?: DevframeDockPreferences
19+
}
1420
}
1521
}
1622

@@ -57,6 +63,13 @@ export interface CreateUiOptions {
5763
* dock only; the standalone viewer is an explicit visit and always shows.
5864
*/
5965
embeddedVisibility?: EmbeddedVisibility
66+
/**
67+
* Dock-bar rendering preferences — category ordering, floating-dock
68+
* inline-item capacity, and the first-run float/edge mode and position.
69+
* Published as `ConnectionMeta.configs.ui.dockPreferences`; each seeds a
70+
* user-overridable preference the visitor's own choice then wins.
71+
*/
72+
dockPreferences?: DevframeDockPreferences
6073
}
6174

6275
/**
@@ -85,9 +98,15 @@ export function createUi(options: CreateUiOptions = {}): DevframeHubUi {
8598
...(options.embedded !== false
8699
? { embedded: { entry: join(client, 'embedded.js') } }
87100
: {}),
88-
configs: () => ({
89-
branding: options.branding || {},
90-
...(options.embeddedVisibility ? { embeddedVisibility: options.embeddedVisibility } : {}),
91-
}),
101+
// Publish the reference UI's config through the generic `ctx.staticConfig`
102+
// — it rides the connection handshake to every mounted frame and the
103+
// standalone viewer as `ConnectionMeta.configs.ui`.
104+
setup(ctx) {
105+
ctx.staticConfig.ui = {
106+
branding: options.branding || {},
107+
...(options.embeddedVisibility ? { embeddedVisibility: options.embeddedVisibility } : {}),
108+
...(options.dockPreferences ? { dockPreferences: options.dockPreferences } : {}),
109+
}
110+
},
92111
}
93112
}

‎packages/hub/src/client/__tests__/host.test.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,9 @@ describe('createDevframeClientHost', () => {
157157
host.dispose()
158158
})
159159

160-
it('merges categoryOrder from connectionMeta.configs.dock beneath DEFAULT_CATEGORIES_ORDER, and the host option beneath that', async () => {
160+
it('merges the host-page categoryOrder option beneath DEFAULT_CATEGORIES_ORDER', async () => {
161161
const { rpc, states } = createStubRpc()
162-
;(rpc as any).connectionMeta = { backend: 'websocket', configs: { dock: { categoryOrder: { app: -200, web: 10 } } } }
163-
const host = await createDevframeClientHost({ rpc, categoryOrder: { web: 999 } })
162+
const host = await createDevframeClientHost({ rpc, categoryOrder: { app: -200, web: 999 } })
164163

165164
states.get('devframe:docks')!.push([
166165
iframeEntry('app-entry', { category: 'app' }),
@@ -169,7 +168,7 @@ describe('createDevframeClientHost', () => {
169168
])
170169

171170
expect(host.context.docks.categoryOrder).toMatchObject({ app: -200, web: 999, framework: -100 })
172-
// `app` (-200, from connectionMeta) now sorts ahead of `framework` (-100, default).
171+
// `app` (-200, from the option) now sorts ahead of `framework` (-100, default).
173172
expect(host.context.docks.groupedEntries.map(([cat]) => cat)).toEqual(['app', 'framework', 'web'])
174173
host.dispose()
175174
})

‎packages/hub/src/client/host.ts‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,9 @@ export interface DevframeClientHostOptions {
7878
* distinct from {@link import('../types/docks').DevframeViewGroup.categoryOrder},
7979
* which only reorders the IN-GROUP sub-categories of one specific group.
8080
*
81-
* Keys are merged over `DEFAULT_CATEGORIES_ORDER` and every installed
82-
* devframe's own declared `dock.categoryOrder` (see
83-
* `ConnectionMeta.configs.dock.categoryOrder`) — this option sorts last
84-
* and wins, so the host app only lists the categories it wants to move;
85-
* any category absent from the map keeps its otherwise-resolved weight.
81+
* Keys are merged over `DEFAULT_CATEGORIES_ORDER`, so the host app only
82+
* lists the categories it wants to move; any category absent from the map
83+
* keeps its default weight.
8684
*
8785
* @example
8886
* ```ts
@@ -138,13 +136,10 @@ export async function createDevframeClientHost(
138136
const frameNavAdapters = new Map<string, () => void>()
139137
const loadScriptsEnabled = options.loadClientScripts ?? true
140138

141-
// Resolved once at boot, fixed for the session: default table, overridden
142-
// by every installed devframe's own declared preference (delivered once
143-
// via the connection handshake), overridden again by this host page's own
144-
// explicit option.
139+
// Resolved once at boot, fixed for the session: the default table overridden
140+
// by this host page's own explicit option.
145141
const categoryOrder: Record<string, number> = {
146142
...DEFAULT_CATEGORIES_ORDER,
147-
...rpc.connectionMeta?.configs?.dock?.categoryOrder,
148143
...options.categoryOrder,
149144
}
150145

‎packages/hub/src/node/__tests__/initiate.test.ts‎

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -174,42 +174,32 @@ describe('initHub', () => {
174174
}
175175
})
176176

177-
it('assembles ConnectionMeta.configs from the ui slot and every installed devframe\'s dock preferences', async () => {
177+
it('serves whatever the ui slot\'s setup(ctx) writes to ctx.staticConfig as ConnectionMeta.configs', async () => {
178178
const wsPort = await getPort({ port: 18225, host: '127.0.0.1' })
179-
const alpha: DevframeDefinition = {
180-
...makeFrame('alpha', makeDist('<!doctype html><title>frame a</title>')),
181-
dock: { category: 'app' },
182-
dockPreferences: { categoryOrder: { app: -40 }, maxVisibleItems: 4 },
183-
}
184-
const beta: DevframeDefinition = {
185-
...makeFrame('beta'),
186-
dock: { category: 'web' },
187-
dockPreferences: { categoryOrder: { app: -60, web: 300 }, defaultMode: 'edge' },
188-
}
179+
const alpha = makeFrame('alpha', makeDist('<!doctype html><title>frame a</title>'))
189180

190181
const hub = initHub({
191182
base: DEVFRAMES_HUB_BASE,
192183
auth: false,
193184
host: '127.0.0.1',
194185
ws: { port: wsPort },
195-
devframes: [alpha, beta],
196-
ui: { configs: () => ({ branding: { productName: 'Test Hub' } }) },
186+
devframes: [alpha],
187+
ui: {
188+
setup(ctx) {
189+
;(ctx.staticConfig as Record<string, unknown>).ui = { branding: { productName: 'Test Hub' } }
190+
},
191+
},
197192
})
198193

199194
try {
200195
await hub.ready
201196
const origin = 'http://localhost:5173'
202197

203-
// The hub's own meta…
198+
// The hub's own meta carries what the ui slot published…
204199
const hubMeta = await (await hub.handler(new Request(`${origin}/__devframes/__connection.json`))).json()
205-
expect(hubMeta.configs).toEqual({
206-
ui: { branding: { productName: 'Test Hub' } },
207-
// Last-installed devframe (`beta`) wins the `app` scalar key; `web`
208-
// only `beta` declared; `maxVisibleItems` only `alpha` declared.
209-
dock: { categoryOrder: { app: -60, web: 300 }, maxVisibleItems: 4, defaultMode: 'edge' },
210-
})
200+
expect(hubMeta.configs).toEqual({ ui: { branding: { productName: 'Test Hub' } } })
211201

212-
// …and every per-frame meta carries the identical aggregate.
202+
// …and every per-frame meta carries the identical document.
213203
const frameMeta = await (await hub.handler(new Request(`${origin}/__devframes/alpha/__connection.json`))).json()
214204
expect(frameMeta.configs).toEqual(hubMeta.configs)
215205
}
@@ -218,7 +208,7 @@ describe('initHub', () => {
218208
}
219209
})
220210

221-
it('omits ConnectionMeta.configs entirely when neither the ui slot nor any devframe declares anything', async () => {
211+
it('omits ConnectionMeta.configs entirely when the ui slot writes nothing', async () => {
222212
const wsPort = await getPort({ port: 18226, host: '127.0.0.1' })
223213
const hub = initHub({ base: DEVFRAMES_HUB_BASE, auth: false, host: '127.0.0.1', ws: { port: wsPort }, devframes: [makeFrame('alpha')] })
224214

0 commit comments

Comments
 (0)