Skip to content

Commit 17213ae

Browse files
antfubotantfu
andauthored
feat(core): treat a grouped dock entry's category as an in-group sub-category (#462)
Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 5853076 commit 17213ae

22 files changed

Lines changed: 647 additions & 258 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Three layers, one mental model:
66

77
- **`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`).
88
- **`@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.
9-
- **`@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.
9+
- **`@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.
1010

1111
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).
1212

@@ -47,7 +47,7 @@ flowchart TD
4747

4848
## Dep Boundary
4949

50-
`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`.
50+
`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`.
5151

5252
`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.
5353

MIGRATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
### What stays the same
1414

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

1919
### If you import from `devframe` directly

docs/kit/dock-system.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,22 @@ A group carries the usual `title`/`icon`/`category`/`defaultOrder`/`when` fields
466466

467467
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`.
468468

469+
### Categories inside a group
470+
471+
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.
472+
473+
```ts
474+
// The group's category ('framework') is the outer bucket for the whole group.
475+
ctx.docks.register({ id: 'nuxt', title: 'Nuxt', icon: 'logos:nuxt-icon', type: 'group', category: 'framework' })
476+
477+
// Members sort into 'app' and 'advanced' SUB-categories inside the Nuxt group,
478+
// while the group button itself lives in 'framework' on the bar.
479+
ctx.docks.register({ id: 'nuxt:overview', title: 'Overview', icon: 'ph:gauge-duotone', type: 'iframe', url: '/__nuxt/overview/', groupId: 'nuxt', category: 'app' })
480+
ctx.docks.register({ id: 'nuxt:graph', title: 'Graph', icon: 'ph:graph-duotone', type: 'iframe', url: '/__nuxt/graph/', groupId: 'nuxt', category: 'advanced' })
481+
```
482+
483+
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`.
484+
469485
### The built-in Vite+ group
470486

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

488504
### Visibility and order
489505

490-
From the dock settings panel, users hide or reorder members within a group independently, and hide the whole group from its row.
506+
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.
491507

492508
## Common options
493509

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

507523
## Update
508524

packages/core/src/client/webcomponents/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/core/src/client/webcomponents/components/dock/DockGroupButton.vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { DevToolsDockEntry, DevToolsViewGroup } from '@vitejs/devtools-kit'
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
44
import { watchDebounced } from '@vueuse/core'
55
import { computed, h, ref, useTemplateRef } from 'vue'
6-
import { getGroupMembers } from '../../state/dock-settings'
6+
import { getGroupMembers, getGroupMembersGrouped } from '../../state/dock-settings'
77
import { sharedStateToRef } from '../../state/docks'
88
import { setDocksGroupPanel, useDocksGroupPanel } from '../../state/floating-tooltip'
99
import DockEntry from './DockEntry.vue'
@@ -29,6 +29,14 @@ const members = computed(() => getGroupMembers(
2929
{ whenContext: props.context.when.context },
3030
))
3131
32+
// Same members, split by in-group sub-category, for the popover's sectioned view.
33+
const membersGrouped = computed(() => getGroupMembersGrouped(
34+
props.context.docks.entries,
35+
props.group.id,
36+
settings.value,
37+
{ whenContext: props.context.when.context },
38+
))
39+
3240
// The group button is "active" while any of its members owns the panel.
3341
const isActive = computed(() => {
3442
const id = props.selected?.id
@@ -48,7 +56,7 @@ function showPanel() {
4856
content: () => h(DockGroupPopover, {
4957
context: props.context,
5058
group: props.group,
51-
members: members.value,
59+
members: membersGrouped.value,
5260
selectedId: props.selected?.id ?? null,
5361
onSelect: (entry: DevToolsDockEntry) => {
5462
emit('select', entry)

packages/core/src/client/webcomponents/components/dock/DockGroupPopover.stories.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import type { Meta, StoryObj } from '@storybook/vue3-vite'
22
import type { DevToolsViewGroup } from '@vitejs/devtools-kit'
3+
import { DEFAULT_STATE_USER_SETTINGS } from '@vitejs/devtools-kit/constants'
34
import { h } from 'vue'
4-
import { group, groupedEntries } from '../../stories/fixtures'
5+
import { getGroupMembersGrouped } from '../../state/dock-settings'
6+
import { group, groupedEntries, subcategorizedGroupEntries, toolsGroup } from '../../stories/fixtures'
57
import { mountWithContext, stage } from '../../stories/story-helpers'
68
import DockGroupPopover from './DockGroupPopover.vue'
79

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

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

78+
/** Members split across in-group sub-categories, shown with section dividers. */
79+
export const WithSubcategories: Story = {
80+
render: () => ({
81+
setup: () => mountWithContext(
82+
{ entries: subcategorizedGroupEntries },
83+
ctx => stage(popover(h(DockGroupPopover, {
84+
context: ctx,
85+
group: toolsGroup,
86+
members: toolsMembers,
87+
selectedId: ctx.docks.selectedId,
88+
onSelect: (entry: any) => ctx.docks.switchEntry(entry.id),
89+
}))),
90+
),
91+
}),
92+
}
93+
7394
/** An empty group falls back to the "No tools yet" placeholder. */
7495
export const Empty: Story = {
7596
render: () => ({

packages/core/src/client/webcomponents/components/dock/DockGroupPopover.vue

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
<script setup lang="ts">
2-
import type { DevToolsDockEntry, DevToolsViewGroup } from '@vitejs/devtools-kit'
2+
import type { DevToolsDockEntriesGrouped, DevToolsDockEntry, DevToolsViewGroup } from '@vitejs/devtools-kit'
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
4+
import { computed } from 'vue'
45
import DockIcon from './DockIcon.vue'
56
6-
defineProps<{
7+
const props = defineProps<{
78
context: DocksContext
89
group: DevToolsViewGroup
9-
members: DevToolsDockEntry[]
10+
/** Members split by in-group sub-category, in display order. */
11+
members: DevToolsDockEntriesGrouped
1012
selectedId: string | null
1113
}>()
1214
1315
const emit = defineEmits<{
1416
(e: 'select', entry: DevToolsDockEntry): void
1517
}>()
18+
19+
const isEmpty = computed(() => props.members.every(([, items]) => items.length === 0))
1620
</script>
1721

1822
<template>
@@ -21,20 +25,24 @@ const emit = defineEmits<{
2125
<DockIcon :icon="group.icon" class="w-4.5 h-4.5" />
2226
<span class="truncate">{{ group.title }}</span>
2327
</div>
24-
<button
25-
v-for="member of members"
26-
:key="member.id"
27-
class="flex items-center gap-2 w-full px2 py1.5 rounded text-sm text-left transition"
28-
:class="selectedId === member.id ? 'text-primary bg-active' : 'op80 hover:op100 hover:bg-active'"
29-
@click="emit('select', member)"
30-
>
31-
<DockIcon :icon="member.icon" class="w-4.5 h-4.5 flex-none" />
32-
<span class="truncate flex-1">{{ member.title }}</span>
33-
<div v-if="member.badge" class="bg-gray-6 text-white text-0.6em px-1 rounded-full shadow">
34-
{{ member.badge }}
35-
</div>
36-
</button>
37-
<div v-if="members.length === 0" class="px2 py1.5 op50 text-sm italic">
28+
<template v-for="([category, items], idx) of members" :key="category">
29+
<!-- Sub-category divider, mirroring the outer bar's category separators -->
30+
<div v-if="idx > 0 && items.length" class="border-t border-base mx--2 my1" />
31+
<button
32+
v-for="member of items"
33+
:key="member.id"
34+
class="flex items-center gap-2 w-full px2 py1.5 rounded text-sm text-left transition"
35+
:class="selectedId === member.id ? 'text-primary bg-active' : 'op80 hover:op100 hover:bg-active'"
36+
@click="emit('select', member)"
37+
>
38+
<DockIcon :icon="member.icon" class="w-4.5 h-4.5 flex-none" />
39+
<span class="truncate flex-1">{{ member.title }}</span>
40+
<div v-if="member.badge" class="bg-gray-6 text-white text-0.6em px-1 rounded-full shadow">
41+
{{ member.badge }}
42+
</div>
43+
</button>
44+
</template>
45+
<div v-if="isEmpty" class="px2 py1.5 op50 text-sm italic">
3846
No tools yet
3947
</div>
4048
</div>

packages/core/src/client/webcomponents/components/dock/DockGroupSidebar.vue

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import type { DevToolsViewGroup } from '@vitejs/devtools-kit'
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
44
import { computed } from 'vue'
5-
import { getGroupMembers } from '../../state/dock-settings'
5+
import { getGroupMembersGrouped } from '../../state/dock-settings'
66
import { sharedStateToRef } from '../../state/docks'
77
import { setFloatingTooltip } from '../../state/floating-tooltip'
88
import DockIcon from './DockIcon.vue'
@@ -15,7 +15,8 @@ const props = defineProps<{
1515
1616
const settings = sharedStateToRef(props.context.docks.settings)
1717
18-
const members = computed(() => getGroupMembers(
18+
// Members split by in-group sub-category so the sidebar can divide sections.
19+
const memberGroups = computed(() => getGroupMembersGrouped(
1920
props.context.docks.entries,
2021
props.group.id,
2122
settings.value,
@@ -47,24 +48,28 @@ function hideTooltip() {
4748
</div>
4849
<div class="w-8 h-px border-t border-base my0.5" />
4950

50-
<!-- Member icons -->
51-
<button
52-
v-for="member of members"
53-
:key="member.id"
54-
class="relative flex items-center justify-center w-8 h-8 rounded-lg transition"
55-
:class="selectedId === member.id ? 'text-primary bg-active' : 'op60 hover:op100 hover:bg-active'"
56-
@pointerenter="showTooltip($event, member.title)"
57-
@pointerleave="hideTooltip"
58-
@pointerdown="hideTooltip"
59-
@click="select(member.id)"
60-
>
61-
<DockIcon :icon="member.icon" class="w-5 h-5 flex-none" />
62-
<div
63-
v-if="member.badge"
64-
class="absolute top-0.5 right-0.5 bg-gray-6 text-white text-0.6em px-0.5 rounded-full shadow leading-none"
51+
<!-- Member icons, grouped by in-group sub-category -->
52+
<template v-for="([category, members], idx) of memberGroups" :key="category">
53+
<!-- Sub-category divider, mirroring the group anchor separator -->
54+
<div v-if="idx > 0 && members.length" class="w-8 h-px border-t border-base my0.5" />
55+
<button
56+
v-for="member of members"
57+
:key="member.id"
58+
class="relative flex items-center justify-center w-8 h-8 rounded-lg transition"
59+
:class="selectedId === member.id ? 'text-primary bg-active' : 'op60 hover:op100 hover:bg-active'"
60+
@pointerenter="showTooltip($event, member.title)"
61+
@pointerleave="hideTooltip"
62+
@pointerdown="hideTooltip"
63+
@click="select(member.id)"
6564
>
66-
{{ member.badge }}
67-
</div>
68-
</button>
65+
<DockIcon :icon="member.icon" class="w-5 h-5 flex-none" />
66+
<div
67+
v-if="member.badge"
68+
class="absolute top-0.5 right-0.5 bg-gray-6 text-white text-0.6em px-0.5 rounded-full shadow leading-none"
69+
>
70+
{{ member.badge }}
71+
</div>
72+
</button>
73+
</template>
6974
</div>
7075
</template>

0 commit comments

Comments
 (0)