Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/kit/dock-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ When the anchor's iframe mounts, Vite DevTools attaches the hub's frame-nav adap

The embedded app stays decoupled: it ships a small `postMessage` nav shim and takes no hub or RPC dependency, so this works cross-origin and in static builds. When no shim answers within the handshake window, the anchor renders as a single plain iframe dock. The protocol, the member-dock data model, and the shim contract live in devframe's [shared-iframe soft-navigation design](https://github.com/devframes/devframe/blob/main/plans/shared-iframe-soft-nav.md).

Set [`visibility: 'false'`](/kit/when-clauses#render-only-visibility) on the anchor when only its synthesized member tabs should have their own dock-bar buttons — the anchor keeps driving the nav loop, but its own button disappears.

## Action buttons

Action buttons run a client-side script when clicked. They suit:
Expand Down Expand Up @@ -559,6 +561,7 @@ Every dock type accepts these base fields:
| `category` | `'app' \| 'framework' \| 'web' \| 'advanced' \| 'default'` | Outer dock-bar bucket, or the in-group sub-category when `groupId` resolves to a group — see [Categories inside a group](#categories-inside-a-group). Defaults to `'default'`. |
| `defaultOrder` | `number` | Orders entries within a category; lower numbers appear first. Default `0`. |
| `when` | `string` | Visibility expression — see [When Clauses](/kit/when-clauses). |
| `visibility` | `string` | Render-only counterpart to `when` — hides just this entry's dock-bar button, leaving it registered and reachable. See [Render-only visibility](/kit/when-clauses#render-only-visibility). |
| `badge` | `string` | Short text badge (e.g. unread count). |
| `groupId` | `string` | Collapse this entry under a group's button; the group's `category` becomes this entry's outer bucket — see [Docked groups](#docked-groups). |

Expand Down
20 changes: 19 additions & 1 deletion docs/kit/when-clauses.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ ctx.docks.register(defineDockEntry({

`when: 'false'` hides a dock entry unconditionally.

### Render-only visibility

`visibility` is a narrower, render-only counterpart to `when`: it hides only the entry's own dock-bar button, while the entry stays registered and fully reachable — `docks.activate()`/`switchEntry()` by id, RPC lookups, and anything else that walks the raw entry list keep working exactly as if it were visible.

The canonical use case is a shared-frame [`subTabs`](/kit/dock-system#shared-iframe-soft-navigation) anchor: 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.

```ts
ctx.docks.register(defineDockEntry({
id: 'my-plugin:anchor',
title: 'Anchor',
type: 'iframe',
url: '/my-plugin/',
icon: 'ph:cursor-duotone',
visibility: 'false', // no button of its own — only its subTabs members render
subTabs: { /* ... */ },
}))
```

## Expression syntax

### Operators
Expand Down Expand Up @@ -134,7 +152,7 @@ Flat keys take priority over nested objects when both exist.

## Type-safe `when` clauses

`defineCommand` and `defineDockEntry` capture the `when:` string as a TypeScript literal and validate it against `WhenContext` through [`whenexpr`](https://github.com/antfu/whenexpr)'s `WhenExpression<Ctx, S>` helper. Syntax errors surface as compile-time errors at the call site.
`defineCommand` and `defineDockEntry` capture the `when:` string as a TypeScript literal and validate it against `WhenContext` through [`whenexpr`](https://github.com/antfu/whenexpr)'s `WhenExpression<Ctx, S>` helper. Syntax errors surface as compile-time errors at the call site. `defineDockEntry` validates `visibility:` the same way.

```ts
import { defineCommand } from '@vitejs/devtools-kit'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ function isDockVisible(dock: DevToolsDockEntry): boolean {
if (members.length === 0)
return false
}
if (!dock.when)
return true
return evaluateWhen(dock.when, props.context.when.context)
if (dock.when && !evaluateWhen(dock.when, props.context.when.context))
return false
// Render-only counterpart to `when`: hides just this button, leaving the
// entry itself registered and reachable everywhere else.
if (dock.visibility && !evaluateWhen(dock.visibility, props.context.when.context))
return false
return true
}

function toggleDockEntry(dock: DevToolsDockEntry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,48 @@ describe('dock groups', () => {
})
})

describe('render-only `visibility` (subTabs anchor use case)', () => {
// A shared-frame subTabs anchor: registered so it keeps driving the
// postMessage nav loop, but hidden from the dock bar in favor of its
// synthesized member tabs rendering their own buttons.
const entries: DevToolsDockEntry[] = [
iframe('anchor', { visibility: 'false', subTabs: {} } as any),
iframe('anchor:overview', { groupId: undefined }),
iframe('a'),
]

it('drops the entry from the rendered dock bar', () => {
const grouped = docksGroupByCategories(entries, settings)
const ids = grouped.flatMap(([, items]) => items.map(i => i.id))
expect(ids).not.toContain('anchor')
expect(ids).toContain('anchor:overview')
expect(ids).toContain('a')
})

it('stays reachable in the raw entries array (activation, RPC, subTabs)', () => {
// `visibility` never removes the entry from the raw list — only grouped/
// rendered results (as produced by `docksGroupByCategories`) omit it.
expect(entries.map(e => e.id)).toContain('anchor')
})

it('still lists the entry in the settings management view (includeHidden)', () => {
const grouped = docksGroupByCategories(entries, settings, { includeHidden: true })
const ids = grouped.flatMap(([, items]) => items.map(i => i.id))
expect(ids).toContain('anchor')
})

it('evaluates `visibility` against a whenContext, same as `when`', () => {
const conditional: DevToolsDockEntry[] = [
iframe('conditional', { visibility: 'clientType == embedded' } as any),
]
const standalone = docksGroupByCategories(conditional, settings, { whenContext: { clientType: 'standalone' } as any })
expect(standalone.flatMap(([, items]) => items.map(i => i.id))).not.toContain('conditional')

const embedded = docksGroupByCategories(conditional, settings, { whenContext: { clientType: 'embedded' } as any })
expect(embedded.flatMap(([, items]) => items.map(i => i.id))).toContain('conditional')
})
})

describe('in-group sub-categories (dual role of `category`)', () => {
// The group carries category 'framework' (the OUTER bucket for the whole
// group); its members carry their own categories, which act as IN-GROUP
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/client/webcomponents/state/dock-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ export function getGroupMembers(
* Filters out hidden entries and categories, then sorts by custom order and
* default order within each category.
*
* Both `when` and its render-only counterpart `visibility` only ever drop an
* entry out of the grouped result *this call* produces — the entry always
* remains in the caller's raw `entries` array, so activation, RPC, and the
* `subTabs` frame-nav adapter (which read `entries` directly rather than a
* grouped result) are unaffected by either clause.
*
* Outer bucketing follows the dual role of `category`: a grouped member whose
* `groupId` resolves to a registered group takes that **group's** `category` as
* its outer bucket (its own `category` is the in-group sub-category instead).
Expand Down Expand Up @@ -237,6 +243,17 @@ export function docksGroupByCategories(
continue
if (entry.when && !whenContext && entry.when === 'false' && !includeHidden)
continue
// Skip if hidden by the render-only `visibility` clause. Unlike `when`,
// `visibility` never affects the entry's registration or reachability —
// it only decides whether *this call* (a dock-bar/popover/sidebar render)
// includes the entry's own button. Callers that need the entry regardless
// (activation, RPC, the `subTabs` frame-nav adapter, the settings
// management view via `includeHidden`) read `entries` directly and are
// unaffected by this check.
if (entry.visibility && whenContext && !evaluateWhen(entry.visibility, whenContext) && !includeHidden)
continue
if (entry.visibility && !whenContext && entry.visibility === 'false' && !includeHidden)
continue
// The Devframe Inspector is hidden by default; it only joins the dock bar
// once opted into via Settings → Advanced. The settings management view
// (`includeHidden`) still lists it so users can discover the toggle.
Expand Down
Loading
Loading