diff --git a/docs/content/1.guide/16.hub.md b/docs/content/1.guide/16.hub.md index 76ac6892..46f2dee2 100644 --- a/docs/content/1.guide/16.hub.md +++ b/docs/content/1.guide/16.hub.md @@ -51,6 +51,22 @@ ctx.commands.register({ `args` takes positional [Standard Schema](https://standardschema.dev/) schemas (a single `v.object(...)` unwraps into the input); omit for zero-arg. `safety` defaults to `'action'`; `when` clauses are unenforced for agent calls. +## Nested commands + +A command's `children` nest arbitrarily deep. The palette drills into each level, and every command in the tree is bindable at any depth — a shortcut assigned to a leaf several levels down fires as directly as one on a top-level command, and each appears as its own row under **Settings → Shortcuts**, indented by nesting level. + +```ts +ctx.commands.register({ + id: 'app:cache', + title: 'Cache', + children: [ + { id: 'app:cache:clear', title: 'Clear', keybindings: [{ key: 'Mod+Shift+K' }], handler: clearCache }, + ], +}) +``` + +Set `showInPalette: 'without-children'` on a parent to keep its whole subtree out of root search while leaving it reachable by drilling down. + ## Cross-iframe dock activation A mounted devframe's iframe uses `hub:docks:activate` to switch the active dock. @@ -199,7 +215,7 @@ ctx.docks.register({ title: 'Nuxt', icon: 'logos:nuxt-icon', category: 'framework', - defaultChildId: 'nuxt:overview', // optional; popover-only when omitted + defaultChildId: 'nuxt:overview', // optional; see "Activating a group" below }) ctx.docks.register({ @@ -212,7 +228,17 @@ ctx.docks.register({ }) ``` -Group and members stay independent top-level entries in `devframe:docks`; `defaultChildId` opens on activation. Grouping affects the dock rail, not iframes — to share **one** soft-navigated iframe, give docks a shared `frameId` and mark the anchor with `subTabs` ([Shared-iframe soft navigation](/guide/client-context#shared-iframe-soft-navigation)). +Group and members stay independent top-level entries in `devframe:docks`. Grouping affects the dock rail, not iframes — to share **one** soft-navigated iframe, give docks a shared `frameId` and mark the anchor with `subTabs` ([Shared-iframe soft navigation](/guide/client-context#shared-iframe-soft-navigation)). + +### Activating a group + +Activating a group resolves to one of its members. + +**Clicking** the dock-rail button opens `defaultChildId` when the group declares one, and reveals the member popover otherwise. + +**By id** — a keyboard shortcut on the group, a command-palette pick, or a `hub:docks:activate` call — opens the member the group points at: `defaultChildId`, or the only visible member when there is exactly one. A group with several members and no `defaultChildId` opens the command palette listing just those members, so the choice stays with the user and the group remains reachable by keyboard alone. Pressing the same shortcut again closes that palette. + +Declare `defaultChildId` when one member is the natural landing spot; leave it off when the members are peers. ### The dual role of `category` diff --git a/docs/content/1.guide/17.client-context.md b/docs/content/1.guide/17.client-context.md index 44f90b2c..99888171 100644 --- a/docs/content/1.guide/17.client-context.md +++ b/docs/content/1.guide/17.client-context.md @@ -40,7 +40,7 @@ A second boot replaces the context and warns; `dispose()` tears down listeners a | `clientType` | `'embedded'` (inside the user app) or `'standalone'` (independent hub page). | | `docks` | `entries`, `selected`, `groupedEntries`, `switchEntry()`, `toggleEntry()`, `getStateById()`, `register()` / `update()` for [client-only docks](#client-only-docks). | | `panel` | Dock panel state: position, size, drag/resize. | -| `commands` | Command palette: `register()`, `execute()`, `getKeybindings()`. | +| `commands` | Command palette: `register()`, `execute()`, `getKeybindings()`, `openPalette(atCommandId?)` — with an id, the palette opens drilled into that command's children and records it in `paletteScopeId` (how [activating a group](/guide/hub#activating-a-group) offers its members). | | `renderers` | Dock-renderer registry — `register()`, `get()`, `has()`, `mount(entry, container)`. Routes a dock `type` to a renderer (local boot or the hub's [manifest](/guide/hub-initiate#renderer-modules); local wins). `mount()` resolves a `status`: `mounted` (with `dispose`), `missing-renderer`, or `load-error` (with `error`). | | `when` | The [when-clause](/references/when-clauses) context. | | `connection` | Live [connection status](/guide/client#handling-connection-and-auth-errors) — `status`, `error`, `events`. | diff --git a/packages/hub-ui/src/client/components/command-palette/CommandPalette.stories.ts b/packages/hub-ui/src/client/components/command-palette/CommandPalette.stories.ts index b6709144..ccf9eb0a 100644 --- a/packages/hub-ui/src/client/components/command-palette/CommandPalette.stories.ts +++ b/packages/hub-ui/src/client/components/command-palette/CommandPalette.stories.ts @@ -45,3 +45,25 @@ export const Open: Story = { ), }), } + +/** + * The palette opened *scoped* to a dock group, listing only that group's + * members — what activating a group with no `defaultChildId` does, so a group + * stays reachable by keyboard with the choice of member left to the user. + * Backspace or Escape steps back out to the root list. + */ +export const ScopedToGroup: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: groupedEntries }, + ctx => h(defineComponent({ + setup() { + onMounted(() => { + ctx.commands.openPalette('devframes:docks:playground') + }) + return () => h(CommandPalette, { context: ctx }) + }, + })), + ), + }), +} diff --git a/packages/hub-ui/src/client/components/command-palette/CommandPalette.vue b/packages/hub-ui/src/client/components/command-palette/CommandPalette.vue index 1aba472c..6e5d1cd1 100644 --- a/packages/hub-ui/src/client/components/command-palette/CommandPalette.vue +++ b/packages/hub-ui/src/client/components/command-palette/CommandPalette.vue @@ -1,8 +1,10 @@