diff --git a/docs/content/1.guide/15.hub.md b/docs/content/1.guide/15.hub.md index bfae9506..84709679 100644 --- a/docs/content/1.guide/15.hub.md +++ b/docs/content/1.guide/15.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 bar, 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 shapes the dock bar; each member keeps its own iframe — 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-bar 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/16.client-context.md b/docs/content/1.guide/16.client-context.md index 81855fe4..4914bb77 100644 --- a/docs/content/1.guide/16.client-context.md +++ b/docs/content/1.guide/16.client-context.md @@ -40,7 +40,7 @@ A second boot replaces the context and warns; `dispose()` tears down listeners a | `clientType` | `'embedded'` (inside your 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](/guide/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 @@