From 4cf1b82df779411329f0bffdcd90943f58360605 Mon Sep 17 00:00:00 2001 From: Maksim Date: Thu, 18 Jun 2026 12:44:34 +0300 Subject: [PATCH 1/2] feat(use-blade): expose activeChildParam (param of the open child blade) --- framework/core/composables/useBlade/index.ts | 12 ++++ .../composables/useBlade/useBlade.test.ts | 67 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/framework/core/composables/useBlade/index.ts b/framework/core/composables/useBlade/index.ts index 05f2b49f2..1add33c25 100644 --- a/framework/core/composables/useBlade/index.ts +++ b/framework/core/composables/useBlade/index.ts @@ -15,6 +15,8 @@ export interface UseBladeReturn> { // Identity (read-only) — runtime error outside blade context readonly id: ComputedRef; readonly param: ComputedRef; + /** param of the direct child blade opened from this blade, or undefined. */ + readonly activeChildParam: ComputedRef; readonly options: ComputedRef; readonly query: ComputedRef | undefined>; readonly closable: ComputedRef; @@ -146,6 +148,15 @@ export function useBlade>(): UseBladeReturn(() => { + if (!descriptor) return undefined; + const selfId = descriptor.value.id; + return bladeStack.blades.value.find((b) => b.parentId === selfId)?.param; + }); + // ── Blade Data ────────────────────────────────────────────────────────── let _bladeDataProvided = false; @@ -319,6 +330,7 @@ export function useBlade>(): UseBladeReturn { expect(() => result.onDeactivated(() => {})).toThrow(/onDeactivated\(\) requires blade context/); }); }); + +// ── activeChildParam ────────────────────────────────────────────────────────── + +describe("useBlade() activeChildParam", () => { + it("returns the param of the direct child blade", () => { + const stack = createMockBladeStack(); + (stack.blades as unknown as { value: unknown[] }).value = [ + { id: "test-blade", parentId: "root", visible: true }, + { id: "child", parentId: "test-blade", param: "order-7", visible: true }, + ]; + const { result } = mountWithBladeContext(() => useBlade(), { + descriptor: { id: "test-blade" }, + stack, + }); + expect(result.activeChildParam.value).toBe("order-7"); + }); + + it("is undefined when the blade has no child", () => { + const stack = createMockBladeStack(); + (stack.blades as unknown as { value: unknown[] }).value = [{ id: "test-blade", parentId: "root", visible: true }]; + const { result } = mountWithBladeContext(() => useBlade(), { + descriptor: { id: "test-blade" }, + stack, + }); + expect(result.activeChildParam.value).toBeUndefined(); + }); + + it("is undefined when the child blade has no param (e.g. add-new)", () => { + const stack = createMockBladeStack(); + (stack.blades as unknown as { value: unknown[] }).value = [ + { id: "test-blade", parentId: "root", visible: true }, + { id: "child", parentId: "test-blade", param: undefined, visible: true }, + ]; + const { result } = mountWithBladeContext(() => useBlade(), { + descriptor: { id: "test-blade" }, + stack, + }); + expect(result.activeChildParam.value).toBeUndefined(); + }); + + it("reacts to a child blade being opened and closed", async () => { + const stack = createMockBladeStack(); + const blades = stack.blades as unknown as { value: unknown[] }; + blades.value = [{ id: "test-blade", parentId: "root", visible: true }]; + const { result } = mountWithBladeContext(() => useBlade(), { + descriptor: { id: "test-blade" }, + stack, + }); + expect(result.activeChildParam.value).toBeUndefined(); + + blades.value = [ + { id: "test-blade", parentId: "root", visible: true }, + { id: "child", parentId: "test-blade", param: "p-1", visible: true }, + ]; + await nextTick(); + expect(result.activeChildParam.value).toBe("p-1"); + + blades.value = [{ id: "test-blade", parentId: "root", visible: true }]; + await nextTick(); + expect(result.activeChildParam.value).toBeUndefined(); + }); + + it("is undefined outside blade context (no throw)", () => { + const { result } = mountWithoutBladeContext(() => useBlade()); + expect(result.activeChildParam.value).toBeUndefined(); + }); +}); From 88977c0f6d849520df4a7e5629e4dc726d142640 Mon Sep 17 00:00:00 2001 From: Maksim Date: Thu, 18 Jun 2026 12:48:19 +0300 Subject: [PATCH 2/2] docs(use-blade): document activeChildParam for active-row highlighting --- .../composables/useBlade/useBlade.docs.md | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/framework/core/composables/useBlade/useBlade.docs.md b/framework/core/composables/useBlade/useBlade.docs.md index 24c2c8ee4..940cea440 100644 --- a/framework/core/composables/useBlade/useBlade.docs.md +++ b/framework/core/composables/useBlade/useBlade.docs.md @@ -103,15 +103,16 @@ Without the generic, `options.value` defaults to `Record | unde These are reactive `ComputedRef` values that reflect the current blade's state. Accessing them outside blade context throws a runtime error. -| Property | Type | Description | -| ---------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | `ComputedRef` | The current blade's unique ID within the blade stack | -| `param` | `ComputedRef` | Route parameter passed when the blade was opened | -| `options` | `ComputedRef` | Additional options object passed to the blade. Type is `Record` by default; provide a generic to get a typed ref: `useBlade()` | -| `query` | `ComputedRef \| undefined>` | URL query parameters scoped to this blade | -| `closable` | `ComputedRef` | `true` when this blade has a parent (i.e., can be closed) | -| `expanded` | `ComputedRef` | `true` when this blade is the active (rightmost) blade | -| `name` | `ComputedRef` | The blade's registered component name | +| Property | Type | Description | +| ------------------ | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | `ComputedRef` | The current blade's unique ID within the blade stack | +| `param` | `ComputedRef` | Route parameter passed when the blade was opened | +| `activeChildParam` | `ComputedRef` | The `param` of the direct child blade opened from this blade (the entity currently shown in the details pane), or `undefined`. Reactive and reload-safe. Bind it to a list table's `:active-item-id` to highlight the open entity's row — it stays correct across reloads, unlike a local ref. | +| `options` | `ComputedRef` | Additional options object passed to the blade. Type is `Record` by default; provide a generic to get a typed ref: `useBlade()` | +| `query` | `ComputedRef \| undefined>` | URL query parameters scoped to this blade | +| `closable` | `ComputedRef` | `true` when this blade has a parent (i.e., can be closed) | +| `expanded` | `ComputedRef` | `true` when this blade is the active (rightmost) blade | +| `name` | `ComputedRef` | The blade's registered component name | #### Navigation Methods (work everywhere) @@ -387,6 +388,30 @@ onDeactivated(() => { }); ``` +### Highlight the row of the open child blade + +Bind `activeChildParam` to the table's `active-item-id` so the row of the currently open details blade is highlighted — including after a page reload: + +```vue + + + +``` + ### Error Management Display or clear error banners on the blade: