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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Three layers, one mental model:

- **`devframe`** — *the container for one devtool integration, portable across viewers.* External project; lives at [`github.com/devframes/devframe`](https://github.com/devframes/devframe), docs at [`devfra.me`](https://devfra.me). Consumed here as an npm dependency (`catalog:deps`).
- **`@devframes/hub`** — *the framework-neutral hub layer on top of devframe.* Owns docks, terminals, messages, commands, the `mountDevframe` primitive, and the json-render factory — anything that only matters once a host wants to combine multiple devframes into one UI. External project, same repo as devframe; consumed via npm.
- **`@vitejs/devtools-kit`** — *the Vite-flavored skin over `@devframes/hub`.* Re-exports hub's hosts and primitives under the kit's `DevTools*` names, adds the Vite-specific extensions (`ViteDevToolsNodeContext`, `PluginWithDevTools`, `DevToolsPluginOptions`, `createViteDevToolsHost`, the `viteplus` dock category), pins the kit-side mount path at `/__devtools/`, and ships `createPluginFromDevframe` to drop a portable devframe into Vite DevTools as a Vite plugin.
- **`@vitejs/devtools-kit`** — *the Vite-flavored skin over `@devframes/hub`.* Re-exports hub's hosts and primitives under the kit's `DevTools*` names, adds the Vite-specific extensions (`ViteDevToolsNodeContext`, `PluginWithDevTools`, `DevToolsPluginOptions`, `createViteDevToolsHost`, the `viteplus` dock group), pins the kit-side mount path at `/__devtools/`, and ships `createPluginFromDevframe` to drop a portable devframe into Vite DevTools as a Vite plugin.

When deciding where something belongs: if a single-app standalone CLI would still need it, it belongs upstream in devframe; if it only matters once a host combines multiple integrations, it belongs in `@devframes/hub` (or in `@vitejs/devtools-kit` if it's Vite-specific).

Expand Down Expand Up @@ -47,7 +47,7 @@ flowchart TD

## Dep Boundary

`devframe` and `@devframes/hub` are external packages consumed via `catalog:deps` — contribute upstream at [github.com/devframes/devframe](https://github.com/devframes/devframe). `packages/kit` and above build on top of them. Features that require multi-integration awareness (docks, terminals, messages, commands) belong upstream in `@devframes/hub`. Features that only matter to Vite — `ViteDevToolsNodeContext`, `PluginWithDevTools`, the `viteplus` category, the kit-pinned `/__devtools/` mount path, the `vite:open-in-editor`/`vite:open-in-finder` commands — stay in `@vitejs/devtools-kit` and `@vitejs/devtools`.
`devframe` and `@devframes/hub` are external packages consumed via `catalog:deps` — contribute upstream at [github.com/devframes/devframe](https://github.com/devframes/devframe). `packages/kit` and above build on top of them. Features that require multi-integration awareness (docks, terminals, messages, commands) belong upstream in `@devframes/hub`. Features that only matter to Vite — `ViteDevToolsNodeContext`, `PluginWithDevTools`, the `viteplus` group, the kit-pinned `/__devtools/` mount path, the `vite:open-in-editor`/`vite:open-in-finder` commands — stay in `@vitejs/devtools-kit` and `@vitejs/devtools`.

`devframe/node/hub-internals` is a marked-public-but-low-level subpath exposing a small set of helpers (`getInternalContext`, `resolveBasePath`) for first-party adapters reaching into devframe's hub-side machinery — kit's adapters use `getInternalContext` for remote-dock token allocation and WS-endpoint metadata. End users should not import it.

Expand Down
2 changes: 1 addition & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
### What stays the same

- The Vite DevTools SPA continues to serve at **`/__devtools/`**. The kit pins this mount path independently of devframe's new `/__devframe/` default.
- `viteplus` remains a valid dock category.
- `viteplus` remains the built-in Vite+ dock **group** id (join it via `groupId: DEVTOOLS_VITEPLUS_GROUP_ID`).
- `vite:open-in-editor` and `vite:open-in-finder` server commands keep their existing IDs.

### If you import from `devframe` directly
Expand Down
22 changes: 19 additions & 3 deletions docs/kit/dock-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,22 @@ A group carries the usual `title`/`icon`/`category`/`defaultOrder`/`when` fields

Membership is a flat pointer, not containment: every member stays an independently-registered top-level entry. A member whose `groupId` references a group that was never registered renders as a normal top-level entry, and a group with no members stays hidden until an entry joins it. Grouping is one level deep — a group entry does not set its own `groupId`.

### Categories inside a group

The `category` field plays a dual role. On a top-level entry it is the outer dock-bar bucket. On a **grouped** member — one whose `groupId` resolves to a registered group — the outer bucket is the **group's** own `category`, and the member's `category` becomes an **in-group sub-category** that divides the group's popover, edge-mode sidebar, settings list, and command-palette drill-down into sections. Sub-categories order by the same category table as the outer bar and default to `default` when unset.

```ts
// The group's category ('framework') is the outer bucket for the whole group.
ctx.docks.register({ id: 'nuxt', title: 'Nuxt', icon: 'logos:nuxt-icon', type: 'group', category: 'framework' })

// Members sort into 'app' and 'advanced' SUB-categories inside the Nuxt group,
// while the group button itself lives in 'framework' on the bar.
ctx.docks.register({ id: 'nuxt:overview', title: 'Overview', icon: 'ph:gauge-duotone', type: 'iframe', url: '/__nuxt/overview/', groupId: 'nuxt', category: 'app' })
ctx.docks.register({ id: 'nuxt:graph', title: 'Graph', icon: 'ph:graph-duotone', type: 'iframe', url: '/__nuxt/graph/', groupId: 'nuxt', category: 'advanced' })
```

An orphan member (its `groupId` matches no registered group) has no group to supply an outer bucket, so it falls back to its own `category`.

### The built-in Vite+ group

Vite DevTools seeds a built-in **Vite+** group that collects Vite ecosystem integrations under one button. Join it with the exported id:
Expand All @@ -487,7 +503,7 @@ DevTools for Rolldown joins this group out of the box.

### Visibility and order

From the dock settings panel, users hide or reorder members within a group independently, and hide the whole group from its row.
From the dock settings panel, users hide or reorder members within a group independently, and hide the whole group from its row. When a group's members span several sub-categories, each sub-category reorders on its own and shows its own header.

## Common options

Expand All @@ -498,11 +514,11 @@ Every dock type accepts these base fields:
| `id` | `string` | Unique, namespaced. |
| `title` | `string` | Label shown in the dock. |
| `icon` | `string \| { light, dark }` | Iconify name, URL, data URI, or light/dark pair. |
| `category` | `'app' \| 'framework' \| 'web' \| 'advanced' \| 'default'` | Grouping in the dock panel. Defaults to `'default'`. |
| `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` | Higher numbers appear first. Default `0`. |
| `when` | `string` | Visibility expression — see [When Clauses](/kit/when-clauses). |
| `badge` | `string` | Short text badge (e.g. unread count). |
| `groupId` | `string` | Collapse this entry under a group's button — see [Docked groups](#docked-groups). |
| `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). |

## Update

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/client/webcomponents/.generated/css.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { DevToolsDockEntry, DevToolsViewGroup } from '@vitejs/devtools-kit'
import type { DocksContext } from '@vitejs/devtools-kit/client'
import { watchDebounced } from '@vueuse/core'
import { computed, h, ref, useTemplateRef } from 'vue'
import { getGroupMembers } from '../../state/dock-settings'
import { getGroupMembers, getGroupMembersGrouped } from '../../state/dock-settings'
import { sharedStateToRef } from '../../state/docks'
import { setDocksGroupPanel, useDocksGroupPanel } from '../../state/floating-tooltip'
import DockEntry from './DockEntry.vue'
Expand All @@ -29,6 +29,14 @@ const members = computed(() => getGroupMembers(
{ whenContext: props.context.when.context },
))

// Same members, split by in-group sub-category, for the popover's sectioned view.
const membersGrouped = computed(() => getGroupMembersGrouped(
props.context.docks.entries,
props.group.id,
settings.value,
{ whenContext: props.context.when.context },
))

// The group button is "active" while any of its members owns the panel.
const isActive = computed(() => {
const id = props.selected?.id
Expand All @@ -48,7 +56,7 @@ function showPanel() {
content: () => h(DockGroupPopover, {
context: props.context,
group: props.group,
members: members.value,
members: membersGrouped.value,
selectedId: props.selected?.id ?? null,
onSelect: (entry: DevToolsDockEntry) => {
emit('select', entry)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import type { DevToolsViewGroup } from '@vitejs/devtools-kit'
import { DEFAULT_STATE_USER_SETTINGS } from '@vitejs/devtools-kit/constants'
import { h } from 'vue'
import { group, groupedEntries } from '../../stories/fixtures'
import { getGroupMembersGrouped } from '../../state/dock-settings'
import { group, groupedEntries, subcategorizedGroupEntries, toolsGroup } from '../../stories/fixtures'
import { mountWithContext, stage } from '../../stories/story-helpers'
import DockGroupPopover from './DockGroupPopover.vue'

const settings = DEFAULT_STATE_USER_SETTINGS()
const nuxtGroup = groupedEntries.find(e => e.id === 'nuxt') as DevToolsViewGroup
const nuxtMembers = groupedEntries.filter(e => e.type !== 'group' && (e as any).groupId === 'nuxt')
// Members split by in-group sub-category (the shape the popover renders).
const nuxtMembers = getGroupMembersGrouped(groupedEntries, 'nuxt', settings)
const toolsMembers = getGroupMembersGrouped(subcategorizedGroupEntries, 'tools', settings)

/** A framed surface that stands in for the floating popover container. */
function popover(children: any) {
Expand Down Expand Up @@ -70,6 +75,22 @@ export const WithBadge: Story = {
}),
}

/** Members split across in-group sub-categories, shown with section dividers. */
export const WithSubcategories: Story = {
render: () => ({
setup: () => mountWithContext(
{ entries: subcategorizedGroupEntries },
ctx => stage(popover(h(DockGroupPopover, {
context: ctx,
group: toolsGroup,
members: toolsMembers,
selectedId: ctx.docks.selectedId,
onSelect: (entry: any) => ctx.docks.switchEntry(entry.id),
}))),
),
}),
}

/** An empty group falls back to the "No tools yet" placeholder. */
export const Empty: Story = {
render: () => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
<script setup lang="ts">
import type { DevToolsDockEntry, DevToolsViewGroup } from '@vitejs/devtools-kit'
import type { DevToolsDockEntriesGrouped, DevToolsDockEntry, DevToolsViewGroup } from '@vitejs/devtools-kit'
import type { DocksContext } from '@vitejs/devtools-kit/client'
import { computed } from 'vue'
import DockIcon from './DockIcon.vue'

defineProps<{
const props = defineProps<{
context: DocksContext
group: DevToolsViewGroup
members: DevToolsDockEntry[]
/** Members split by in-group sub-category, in display order. */
members: DevToolsDockEntriesGrouped
selectedId: string | null
}>()

const emit = defineEmits<{
(e: 'select', entry: DevToolsDockEntry): void
}>()

const isEmpty = computed(() => props.members.every(([, items]) => items.length === 0))
</script>

<template>
Expand All @@ -21,20 +25,24 @@ const emit = defineEmits<{
<DockIcon :icon="group.icon" class="w-4.5 h-4.5" />
<span class="truncate">{{ group.title }}</span>
</div>
<button
v-for="member of members"
:key="member.id"
class="flex items-center gap-2 w-full px2 py1.5 rounded text-sm text-left transition"
:class="selectedId === member.id ? 'text-primary bg-active' : 'op80 hover:op100 hover:bg-active'"
@click="emit('select', member)"
>
<DockIcon :icon="member.icon" class="w-4.5 h-4.5 flex-none" />
<span class="truncate flex-1">{{ member.title }}</span>
<div v-if="member.badge" class="bg-gray-6 text-white text-0.6em px-1 rounded-full shadow">
{{ member.badge }}
</div>
</button>
<div v-if="members.length === 0" class="px2 py1.5 op50 text-sm italic">
<template v-for="([category, items], idx) of members" :key="category">
<!-- Sub-category divider, mirroring the outer bar's category separators -->
<div v-if="idx > 0 && items.length" class="border-t border-base mx--2 my1" />
<button
v-for="member of items"
:key="member.id"
class="flex items-center gap-2 w-full px2 py1.5 rounded text-sm text-left transition"
:class="selectedId === member.id ? 'text-primary bg-active' : 'op80 hover:op100 hover:bg-active'"
@click="emit('select', member)"
>
<DockIcon :icon="member.icon" class="w-4.5 h-4.5 flex-none" />
<span class="truncate flex-1">{{ member.title }}</span>
<div v-if="member.badge" class="bg-gray-6 text-white text-0.6em px-1 rounded-full shadow">
{{ member.badge }}
</div>
</button>
</template>
<div v-if="isEmpty" class="px2 py1.5 op50 text-sm italic">
No tools yet
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { DevToolsViewGroup } from '@vitejs/devtools-kit'
import type { DocksContext } from '@vitejs/devtools-kit/client'
import { computed } from 'vue'
import { getGroupMembers } from '../../state/dock-settings'
import { getGroupMembersGrouped } from '../../state/dock-settings'
import { sharedStateToRef } from '../../state/docks'
import { setFloatingTooltip } from '../../state/floating-tooltip'
import DockIcon from './DockIcon.vue'
Expand All @@ -15,7 +15,8 @@ const props = defineProps<{

const settings = sharedStateToRef(props.context.docks.settings)

const members = computed(() => getGroupMembers(
// Members split by in-group sub-category so the sidebar can divide sections.
const memberGroups = computed(() => getGroupMembersGrouped(
props.context.docks.entries,
props.group.id,
settings.value,
Expand Down Expand Up @@ -47,24 +48,28 @@ function hideTooltip() {
</div>
<div class="w-8 h-px border-t border-base my0.5" />

<!-- Member icons -->
<button
v-for="member of members"
:key="member.id"
class="relative flex items-center justify-center w-8 h-8 rounded-lg transition"
:class="selectedId === member.id ? 'text-primary bg-active' : 'op60 hover:op100 hover:bg-active'"
@pointerenter="showTooltip($event, member.title)"
@pointerleave="hideTooltip"
@pointerdown="hideTooltip"
@click="select(member.id)"
>
<DockIcon :icon="member.icon" class="w-5 h-5 flex-none" />
<div
v-if="member.badge"
class="absolute top-0.5 right-0.5 bg-gray-6 text-white text-0.6em px-0.5 rounded-full shadow leading-none"
<!-- Member icons, grouped by in-group sub-category -->
<template v-for="([category, members], idx) of memberGroups" :key="category">
<!-- Sub-category divider, mirroring the group anchor separator -->
<div v-if="idx > 0 && members.length" class="w-8 h-px border-t border-base my0.5" />
<button
v-for="member of members"
:key="member.id"
class="relative flex items-center justify-center w-8 h-8 rounded-lg transition"
:class="selectedId === member.id ? 'text-primary bg-active' : 'op60 hover:op100 hover:bg-active'"
@pointerenter="showTooltip($event, member.title)"
@pointerleave="hideTooltip"
@pointerdown="hideTooltip"
@click="select(member.id)"
>
{{ member.badge }}
</div>
</button>
<DockIcon :icon="member.icon" class="w-5 h-5 flex-none" />
<div
v-if="member.badge"
class="absolute top-0.5 right-0.5 bg-gray-6 text-white text-0.6em px-0.5 rounded-full shadow leading-none"
>
{{ member.badge }}
</div>
</button>
</template>
</div>
</template>
Loading
Loading