@vc-shell/vc-app-skill 2.9.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vc-shell/vc-app-skill",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "AI coding skill for scaffolding and generating VirtoCommerce Shell applications. Works with Claude Code, OpenCode, Gemini, Codex, Cursor.",
5
5
  "bin": "./bin/install.cjs",
6
6
  "files": [
package/runtime/VERSION CHANGED
@@ -1 +1 @@
1
- 2.9.0
1
+ 2.10.0
@@ -1 +1 @@
1
- Synced from framework at commit fd1e671ba on 2026-11-02T09:58:34.572Z
1
+ Synced from framework at commit 413c13a36 on 2026-11-02T10:54:29.692Z
@@ -101,13 +101,13 @@ defineBladeContext(
101
101
 
102
102
  ## Recipe: Consumers Registered from Another Module
103
103
 
104
- Toolbar buttons and headless widgets registered by blade name from another module are plain objects, so `injectBladeContext()` has no component to run in. The framework hands them the context instead:
104
+ Toolbar buttons and headless widgets registered by blade name from another module are plain objects, so `injectBladeContext()` has no component to run in. The framework calls their function forms with two arguments instead: `(blade, ctx)`, the descriptor of the blade the button or widget sits on and its context. The blade comes first because `isVisible: (blade) => …` has worked that way since 1.1.37.
105
105
 
106
- | Registration | Receives the context |
107
- | ----------------------------------------------------- | --------------------------------------------------------------------------------- |
108
- | `registerExternalToolbarItem` / `registerToolbarItem` | `clickHandler(ctx)`, `disabled: (ctx) => …`, `isVisible: (blade, ctx) => …` |
109
- | `registerExternalWidget` / `registerWidget` | `isVisible: (blade, ctx) => …`; the component itself calls `injectBladeContext()` |
110
- | `registerWidget` with `headless` | `headless.onClick(ctx)`, `headless.disabled: (ctx) => …` |
106
+ | Registration | Called with `(blade, ctx)` |
107
+ | ----------------------------------------------------- | -------------------------------------------------------------- |
108
+ | `registerExternalToolbarItem` / `registerToolbarItem` | `clickHandler`, `disabled`, `isVisible` |
109
+ | `registerExternalWidget` / `registerWidget` | `isVisible`; the component itself calls `injectBladeContext()` |
110
+ | `registerWidget` with `headless` | `headless.onClick`, `headless.disabled` |
111
111
 
112
112
  ```typescript
113
113
  // In the blade
@@ -118,13 +118,24 @@ registerExternalToolbarItem({
118
118
  id: "bulk-export",
119
119
  title: "Export selected",
120
120
  targetBlades: ["Products"],
121
- disabled: (ctx) => !(ctx.selectedItems as Product[] | undefined)?.length,
122
- clickHandler: (ctx) => exportProducts(ctx?.selectedItems as Product[]),
121
+ disabled: (_blade, ctx) => !(ctx.selectedItems as Product[] | undefined)?.length,
122
+ clickHandler: (_blade, ctx) => exportProducts(ctx.selectedItems as Product[]),
123
123
  });
124
124
  ```
125
125
 
126
126
  The values are the same unwrapped snapshot `injectBladeContext()` returns, and the state is reactive: `disabled` and `isVisible` follow the blade's refs. A blade without `defineBladeContext` gives these handlers `{}`, not an error.
127
127
 
128
+ `ctx` does not say which blade it came from. When one registration targets several blades, tell them apart by `blade.name`:
129
+
130
+ ```typescript
131
+ registerExternalToolbarItem({
132
+ id: "bulk-export",
133
+ targetBlades: ["Products", "Offers"],
134
+ disabled: (_blade, ctx) => !(ctx.selectedItems as unknown[] | undefined)?.length,
135
+ clickHandler: (blade, ctx) => (blade?.name === "Offers" ? exportOffers(ctx.selectedItems as Offer[]) : exportProducts(ctx.selectedItems as Product[])),
136
+ });
137
+ ```
138
+
128
139
  ## Details
129
140
 
130
141
  - **Automatic ref unwrapping**: `defineBladeContext` shallow-unwraps all ref/computed values in the provided object. Consumers get plain values directly (`ctx.value.item` instead of `ctx.value.item.value`). This works reactively — when the source ref changes, the context updates automatically.
@@ -199,7 +199,7 @@ The name match is case-insensitive. If the blade registers its own item with the
199
199
 
200
200
  ### Reading the blade's data
201
201
 
202
- A button registered from outside cannot call `injectBladeContext()`: its handlers run outside the blade's component tree. The framework passes them the data the blade exposed with [`defineBladeContext`](../bladeContext/) instead. `clickHandler` receives it, and so do the function forms of `disabled` and `isVisible`:
202
+ A button registered from outside cannot call `injectBladeContext()`: its handlers run outside the blade's component tree. The framework calls `clickHandler` and the function forms of `disabled` and `isVisible` with `(blade, ctx)`: the descriptor of the blade the button sits on, and the data it exposed with [`defineBladeContext`](../bladeContext/).
203
203
 
204
204
  ```typescript
205
205
  // In the target blade
@@ -211,12 +211,22 @@ registerExternalToolbarItem({
211
211
  title: "Export selected",
212
212
  icon: "lucide-download",
213
213
  targetBlades: ["Products"],
214
- disabled: (ctx) => !(ctx.selectedItems as Product[] | undefined)?.length,
215
- clickHandler: (ctx) => exportProducts(ctx?.selectedItems as Product[]),
214
+ disabled: (_blade, ctx) => !(ctx.selectedItems as Product[] | undefined)?.length,
215
+ clickHandler: (_blade, ctx) => exportProducts(ctx.selectedItems as Product[]),
216
216
  });
217
217
  ```
218
218
 
219
- Refs are already unwrapped, and the state follows the blade: the button enables as soon as the user selects a row. A keyboard `shortcut` on the item gets the same context. When the blade defines no context, the handlers get `{}`.
219
+ Refs are already unwrapped, and the state follows the blade: the button enables as soon as the user selects a row. A keyboard `shortcut` on the item gets the same arguments. When the blade defines no context, `ctx` is `{}`.
220
+
221
+ `ctx` does not say which blade it came from. With several `targetBlades`, branch on `blade.name`:
222
+
223
+ ```typescript
224
+ registerExternalToolbarItem({
225
+ id: "bulk-export",
226
+ targetBlades: ["Products", "Offers"],
227
+ clickHandler: (blade, ctx) => (blade?.name === "Offers" ? exportOffers(ctx.selectedItems as Offer[]) : exportProducts(ctx.selectedItems as Product[])),
228
+ });
229
+ ```
220
230
 
221
231
  ## Auto-cleanup control
222
232
 
@@ -404,9 +414,9 @@ function helperFunction() {
404
414
  | `id` | `string` | Yes | Unique identifier for the button |
405
415
  | `title` | `string \| Ref<string> \| ComputedRef<string>` | No | Button label (supports reactive values) |
406
416
  | `icon` | `string \| (() => string)` | No | Icon class (e.g., `"fas fa-save"`) or a function returning one |
407
- | `clickHandler` | `(context?: BladeContext) => void` | No | Click callback; receives the blade context |
408
- | `disabled` | `boolean \| ComputedRef<boolean \| undefined> \| ((context: BladeContext) => boolean \| undefined)` | No | Whether the button is disabled; a function receives the blade context |
409
- | `isVisible` | `boolean \| Ref<boolean \| undefined> \| ComputedRef<boolean \| undefined> \| ((blade, context) => boolean \| undefined)` | No | Controls button visibility; a function receives the blade descriptor and the blade context |
417
+ | `clickHandler` | `(blade, context) => void` | No | Click callback; receives the blade and its context |
418
+ | `disabled` | `boolean \| ComputedRef<boolean \| undefined> \| ((blade, context) => boolean \| undefined)` | No | Whether the button is disabled; a function receives the blade and its context |
419
+ | `isVisible` | `boolean \| Ref<boolean \| undefined> \| ComputedRef<boolean \| undefined> \| ((blade, context) => boolean \| undefined)` | No | Controls button visibility; a function receives the blade and its context |
410
420
  | `priority` | `number` | No | Sort order (higher = displayed first, default `0`) |
411
421
  | `separator` | `"left" \| "right" \| "both"` | No | Adds a visual divider next to the button |
412
422
  | `permissions` | `string \| string[]` | No | Required permission(s) to display the button |
@@ -118,7 +118,7 @@ export default {
118
118
  };
119
119
  ```
120
120
 
121
- The widget component reads the blade's data with [`injectBladeContext()`](../bladeContext/), since it renders inside the blade. `isVisible` runs before the component exists, so a function form receives the blade context as its second argument:
121
+ The widget component reads the blade's data with [`injectBladeContext()`](../bladeContext/), since it renders inside the blade. `isVisible` runs before the component exists, so a function form receives `(blade, ctx)`: the blade descriptor and the blade context.
122
122
 
123
123
  ```typescript
124
124
  registerExternalWidget({
@@ -126,11 +126,11 @@ registerExternalWidget({
126
126
  targetBlades: ["OrderDetails"],
127
127
  component: markRaw(ShippingTracker),
128
128
  // the blade calls defineBladeContext({ item })
129
- isVisible: (blade, ctx) => (ctx.item as Order | undefined)?.status === "Shipped",
129
+ isVisible: (_blade, ctx) => (ctx.item as Order | undefined)?.status === "Shipped",
130
130
  });
131
131
  ```
132
132
 
133
- A headless widget registered by blade name has no component to inject from, so its `onClick` and a function `disabled` receive the context too:
133
+ A headless widget registered by blade name has no component to inject from, so its `onClick` and a function `disabled` receive `(blade, ctx)` too:
134
134
 
135
135
  ```typescript
136
136
  registerWidget(
@@ -139,8 +139,8 @@ registerWidget(
139
139
  title: "Invoice",
140
140
  headless: {
141
141
  icon: "lucide-receipt",
142
- disabled: (ctx) => !ctx.item,
143
- onClick: (ctx) => openInvoice((ctx?.item as Order).id),
142
+ disabled: (_blade, ctx) => !ctx.item,
143
+ onClick: (_blade, ctx) => openInvoice((ctx.item as Order).id),
144
144
  },
145
145
  },
146
146
  "OrderDetails",
@@ -359,9 +359,10 @@ interface IBladeToolbar {
359
359
  id?: string;
360
360
  title?: string | Ref<string> | ComputedRef<string>;
361
361
  icon?: string | (() => string);
362
- clickHandler?(app?: Record<string, any> | null): void;
363
- disabled?: boolean | ComputedRef<boolean | undefined>;
364
- isVisible?: boolean | Ref<boolean | undefined> | ComputedRef<boolean | undefined> | ((blade?: BladeDescriptor) => boolean | undefined);
362
+ // function forms get (blade, context): the blade descriptor and the blade context
363
+ clickHandler?(blade: BladeDescriptor | undefined, context: BladeContext): void;
364
+ disabled?: boolean | ComputedRef<boolean | undefined> | ((blade: BladeDescriptor | undefined, context: BladeContext) => boolean | undefined);
365
+ isVisible?: boolean | Ref<boolean | undefined> | ComputedRef<boolean | undefined> | ((blade: BladeDescriptor | undefined, context: BladeContext) => boolean | undefined);
365
366
  separator?: "left" | "right" | "both";
366
367
  permissions?: string | string[];
367
368
  }