Skip to content

Commit c359ac1

Browse files
authored
feat(hub): add render-only visibility to dock entry base declaration (#136)
1 parent 602367b commit c359ac1

7 files changed

Lines changed: 91 additions & 4 deletions

File tree

docs/guide/when-clauses.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ ctx.docks.register({
4040

4141
`when: 'false'` hides an entry unconditionally.
4242

43+
### Render-only visibility on dock entries
44+
45+
A dock entry also takes `visibility`, a second expression in the same syntax that hides only the entry's own dock-bar button, leaving the entry registered and reachable — `docks.activate()`/`switchEntry()` by id, RPC lookups, and anything else that walks the raw entry list keep working. `when` remains the general relevance switch for the entry as a whole; reach for `visibility` only when an entry must stay in the model without a button of its own.
46+
47+
The canonical case is a shared-frame anchor (`subTabs`): the anchor iframe must stay registered to keep driving the postMessage nav loop, but only its synthesized member tabs should render as dock-bar buttons.
48+
49+
```ts
50+
ctx.docks.register({
51+
id: 'my-devtool:anchor',
52+
title: 'My Devtool',
53+
type: 'iframe',
54+
icon: 'ph:squares-four-duotone',
55+
url: '/__my-devtool/',
56+
subTabs: { protocol: 'postmessage' },
57+
visibility: 'false', // hide the anchor's own button; its tabs still render
58+
})
59+
```
60+
4361
## Expression syntax
4462

4563
### Operators

packages/devframe/src/types/devframe.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,13 @@ export interface DevframeDockDefaults {
219219
* clauses). Set to `'false'` to hide the entry unconditionally.
220220
*/
221221
when?: string
222+
/**
223+
* Render-only visibility expression, same syntax as {@link when}. Hides the
224+
* entry's own dock-bar button when it evaluates to `false` while leaving it
225+
* registered and reachable (activation, RPC lookups, etc.) — unlike `when`,
226+
* which is the general relevance switch for the entry as a whole.
227+
*/
228+
visibility?: string
222229
/** Badge text rendered on the dock icon (e.g. an unread count). */
223230
badge?: string
224231
/** Id of the dock group this entry collapses under, if any. */

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,24 @@ describe('createDevframeClientHost', () => {
9999
host.dispose()
100100
})
101101

102+
it('keeps a `visibility: false` entry in the raw model, activatable by id', async () => {
103+
const { rpc, states } = createStubRpc()
104+
const host = await createDevframeClientHost({ rpc })
105+
states.get('devframe:docks')!.push([
106+
iframeEntry('anchor', { subTabs: { protocol: 'postmessage' }, visibility: 'false' }),
107+
iframeEntry('visible'),
108+
])
109+
110+
// `visibility` is a render-only hint for the UI layer — the hub itself
111+
// never filters `entries`/`getStateById`/`switchEntry` by it, so the
112+
// anchor stays fully reachable.
113+
expect(host.context.docks.entries.map(e => e.id)).toEqual(['anchor', 'visible'])
114+
expect(host.context.docks.getStateById('anchor')).toBeDefined()
115+
expect(await host.context.docks.switchEntry('anchor')).toBe(true)
116+
expect(host.context.docks.selected?.id).toBe('anchor')
117+
host.dispose()
118+
})
119+
102120
it('groups entries by category — grouped members bucket under their group, orphans by their own', async () => {
103121
const { rpc, states } = createStubRpc()
104122
const host = await createDevframeClientHost({ rpc })

packages/hub/src/define.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ export function defineCommand<const W extends string = ''>(
1616
export function defineDockEntry<
1717
const T extends DevframeDockUserEntry,
1818
const W extends string = '',
19+
const V extends string = '',
1920
>(
20-
entry: Omit<T, 'when'> & { when?: WhenExpression<WhenContext, W> },
21+
entry: Omit<T, 'when' | 'visibility'> & {
22+
when?: WhenExpression<WhenContext, W>
23+
visibility?: WhenExpression<WhenContext, V>
24+
},
2125
): T {
2226
return entry as unknown as T
2327
}

packages/hub/src/node/__tests__/mount-devframe.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ describe('mountDevframe', () => {
7171
})
7272
})
7373

74+
it('applies the definition-level dock `visibility` default to the synthesized entry, independent of `when`', async () => {
75+
const ctx = createContext()
76+
await mountDevframe(ctx, makeDevframe({
77+
dock: { when: 'clientType == embedded', visibility: 'false' },
78+
}))
79+
80+
expect(ctx.docks.views.get('demo')).toMatchObject({
81+
id: 'demo',
82+
when: 'clientType == embedded',
83+
visibility: 'false',
84+
})
85+
})
86+
7487
it('lets per-mount dock overrides win over the definition dock defaults', async () => {
7588
const ctx = createContext()
7689
await mountDevframe(

packages/hub/src/node/mount-devframe.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ export interface MountDevframeOptions {
1313
/**
1414
* Per-mount overrides for the auto-synthesized iframe dock entry. Use
1515
* this to customize the entry's `category`, override the icon, hide it
16-
* via `when`, etc. Takes precedence over the definition's own
17-
* {@link DevframeDefinition.dock} defaults. Cannot change `id`, `type`,
18-
* or `url` — those are derived from the devframe definition.
16+
* via `when` (or only its dock-bar button via `visibility`), etc. Takes
17+
* precedence over the definition's own {@link DevframeDefinition.dock}
18+
* defaults. Cannot change `id`, `type`, or `url` — those are derived from
19+
* the devframe definition.
1920
*/
2021
dock?: Partial<Omit<DevframeViewIframe, 'id' | 'type' | 'url'>>
2122
}

packages/hub/src/types/docks.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ export interface DevframeDockEntryBase {
108108
* @see {@link import('devframe/utils/when').evaluateWhen}
109109
*/
110110
when?: string
111+
/**
112+
* Render-only conditional visibility expression, same syntax as {@link when}.
113+
* When it evaluates to `false`, a viewer omits the entry from the rendered
114+
* dock bar / list, but the entry stays registered and fully reachable —
115+
* `docks.activate()`/`switchEntry()` by id, RPC lookups, and anything else
116+
* that walks the raw entry list (e.g. the {@link DevframeViewIframe.subTabs}
117+
* frame-nav adapter) keep working exactly as if it were visible.
118+
*
119+
* Use this instead of {@link when} when an entry must remain part of the
120+
* model without a dock-bar button of its own — the canonical case is a
121+
* shared-frame {@link DevframeViewIframe.subTabs anchor}: set
122+
* `visibility: 'false'` on the anchor so only its synthesized member tabs
123+
* render, while the anchor itself keeps driving the postMessage nav loop.
124+
* `when`, by contrast, is the general relevance switch for the entry as a
125+
* whole; reach for `visibility` only for this render-only carve-out.
126+
*
127+
* Set to `'false'` to unconditionally hide the entry's own dock-bar button.
128+
*
129+
* @example 'false'
130+
* @see {@link import('devframe/utils/when').evaluateWhen}
131+
*/
132+
visibility?: string
111133
/**
112134
* Badge text to display on the dock icon (e.g., unread count)
113135
*/
@@ -182,6 +204,10 @@ export interface DevframeViewIframe extends DevframeDockEntryBase {
182204
* (grouped/soft-navigated via this anchor's {@link frameId}), and drives the
183205
* live navigation loop. Absent a shim, the anchor simply renders as a single
184206
* plain iframe dock.
207+
*
208+
* Set {@link DevframeDockEntryBase.visibility} to `'false'` on the anchor to
209+
* hide its own dock-bar button once tabs are discovered, surfacing only the
210+
* synthesized member docks while the anchor keeps driving the nav loop.
185211
*/
186212
subTabs?: FrameSubTabsConfig
187213
/**

0 commit comments

Comments
 (0)