From 50a217898a4a887f3aeb9f1d2201da2af0279c2f Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Wed, 15 Jul 2026 07:39:16 +0000 Subject: [PATCH] feat(hub): support dock defaults on DevframeDefinition Add a `dock` option to DevframeDefinition carrying default dock attributes for the iframe entry a hub synthesizes. mountDevframe merges these beneath its per-mount `dock` overrides, which in turn sit beneath the locked, derived id/type/url. --- packages/devframe/src/types/devframe.ts | 45 +++++++++++++++++++ .../src/node/__tests__/mount-devframe.test.ts | 30 +++++++++++++ packages/hub/src/node/mount-devframe.ts | 13 ++++-- .../tsnapi/devframe/index.snapshot.d.ts | 1 + .../tsnapi/devframe/types.snapshot.d.ts | 1 + 5 files changed, 86 insertions(+), 4 deletions(-) diff --git a/packages/devframe/src/types/devframe.ts b/packages/devframe/src/types/devframe.ts index e4015470..6709fe43 100644 --- a/packages/devframe/src/types/devframe.ts +++ b/packages/devframe/src/types/devframe.ts @@ -175,6 +175,42 @@ export interface DevframeCliOptions { flags?: CliFlagsSchema } +/** + * Default dock attributes for the iframe entry a hub synthesizes when it + * mounts this devframe. Framework-neutral metadata only — the hub layer + * (`mountDevframe`) merges these beneath its per-mount `dock` overrides, + * which in turn sit beneath the locked, derived `id` / `type` / `url`. + * + * Every field is optional. `title` / `icon` default to the definition's + * `name` / `icon` when omitted here; the rest are unset by default. + * Standalone adapters (`cli` / `spa` / `build`) ignore this entirely. + */ +export interface DevframeDockDefaults { + /** Dock entry title. Defaults to the definition's `name`. */ + title?: string + /** Dock entry icon. Defaults to the definition's `icon`. */ + icon?: string | { light: string, dark: string } + /** + * Sort weight within the dock; higher sorts earlier. + * @default 0 + */ + defaultOrder?: number + /** + * Category the entry groups under in the dock. + * @default 'default' + */ + category?: string + /** + * Conditional-visibility expression (same syntax as command `when` + * clauses). Set to `'false'` to hide the entry unconditionally. + */ + when?: string + /** Badge text rendered on the dock icon (e.g. an unread count). */ + badge?: string + /** Id of the dock group this entry collapses under, if any. */ + groupId?: string +} + export interface DevframeSpaOptions { base?: string /** @@ -215,6 +251,15 @@ export interface DevframeDefinition { /** One-line summary of what the tool does. */ description: string icon?: string | { light: string, dark: string } + /** + * Default dock attributes applied when a hub mounts this devframe as an + * iframe dock entry. Consulted only by hub adapters (`mountDevframe`), + * which merge these beneath the per-mount `dock` overrides; standalone + * adapters (`cli` / `spa` / `build`) ignore it. + * + * @see {@link DevframeDockDefaults} + */ + dock?: DevframeDockDefaults /** * Mount path override. Defaults depend on the adapter: * `/` for standalone (`cli` / `spa` / `build`), `/__/` for hosted diff --git a/packages/hub/src/node/__tests__/mount-devframe.test.ts b/packages/hub/src/node/__tests__/mount-devframe.test.ts index 9da39245..f4c6cc09 100644 --- a/packages/hub/src/node/__tests__/mount-devframe.test.ts +++ b/packages/hub/src/node/__tests__/mount-devframe.test.ts @@ -55,6 +55,36 @@ describe('mountDevframe', () => { expect(setup).toHaveBeenCalledTimes(1) }) + it('applies the definition-level dock defaults to the synthesized entry', async () => { + const ctx = createContext() + await mountDevframe(ctx, makeDevframe({ + dock: { category: 'framework', defaultOrder: 5, when: 'clientType == embedded' }, + })) + + expect(ctx.docks.views.get('demo')).toMatchObject({ + id: 'demo', + title: 'Demo', + type: 'iframe', + category: 'framework', + defaultOrder: 5, + when: 'clientType == embedded', + }) + }) + + it('lets per-mount dock overrides win over the definition dock defaults', async () => { + const ctx = createContext() + await mountDevframe( + ctx, + makeDevframe({ dock: { category: 'framework', defaultOrder: 5 } }), + { dock: { category: 'app' } }, + ) + + expect(ctx.docks.views.get('demo')).toMatchObject({ + category: 'app', + defaultOrder: 5, + }) + }) + it('warns and deduplicates by default, keeping the first registration', async () => { const ctx = createContext() const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) diff --git a/packages/hub/src/node/mount-devframe.ts b/packages/hub/src/node/mount-devframe.ts index 935ebd24..9839c889 100644 --- a/packages/hub/src/node/mount-devframe.ts +++ b/packages/hub/src/node/mount-devframe.ts @@ -11,10 +11,11 @@ export interface MountDevframeOptions { */ base?: string /** - * Overrides for the auto-synthesized iframe dock entry. Use this to - * customize the entry's `category`, override the icon, hide it via - * `when`, etc. Cannot change `id`, `type`, or `url` — those are - * derived from the devframe definition. + * Per-mount overrides for the auto-synthesized iframe dock entry. Use + * this to customize the entry's `category`, override the icon, hide it + * via `when`, etc. Takes precedence over the definition's own + * {@link DevframeDefinition.dock} defaults. Cannot change `id`, `type`, + * or `url` — those are derived from the devframe definition. */ dock?: Partial> } @@ -85,6 +86,10 @@ export async function mountDevframe( id, title: d.name, icon: d.icon ?? 'ph:plug-duotone', + // Definition-level `dock` defaults sit above the name/icon-derived + // defaults; per-mount `options.dock` overrides them; `type`/`url` + // (and `id`) stay locked, derived from the definition. + ...d.dock, ...options.dock, type: 'iframe', url: base, diff --git a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts index 606fd0c0..f2b30d78 100644 --- a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts @@ -27,6 +27,7 @@ export { DevframeDeploymentKind } export { DevframeDiagnosticsDefinition } export { DevframeDiagnosticsHost } export { DevframeDiagnosticsLogger } +export { DevframeDockDefaults } export { DevframeDuplicationStrategy } export { DevframeHost } export { DevframeNodeContext } diff --git a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts index 92dd021e..399512d0 100644 --- a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts @@ -23,6 +23,7 @@ export { DevframeDeploymentKind } export { DevframeDiagnosticsDefinition } export { DevframeDiagnosticsHost } export { DevframeDiagnosticsLogger } +export { DevframeDockDefaults } export { DevframeDuplicationStrategy } export { DevframeHost } export { DevframeNodeContext }