@vc-shell/vc-app-skill 2.0.0 → 2.0.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.2](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.1...v2.0.2) (2026-04-27)
4
+
5
+ **Note:** Version bump only for package @vc-shell/vc-app-skill
6
+
7
+ ## [2.0.1](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0...v2.0.1) (2026-04-24)
8
+
9
+ ### Features
10
+
11
+ - enhance migrate transforms, fix notification race condition, update migration docs (#221) ([3c2134d](https://github.com/VirtoCommerce/vc-shell/commit/3c2134d0fa1016a2e4e075a12bb9df9a31b3d381)), closes [#221](https://github.com/VirtoCommerce/vc-shell/issues/221)
12
+
13
+ ### Bug Fixes
14
+
15
+ - **hooks:** update npm version retrieval in vc-app-check-update.js ([c8f0e68](https://github.com/VirtoCommerce/vc-shell/commit/c8f0e68ded994bdfb8d01dcf96e90184b9d278cd))
16
+
3
17
  # [2.0.0](https://github.com/VirtoCommerce/vc-shell/compare/v1.2.3...v2.0.0) (2026-04-22)
4
18
 
5
19
  ### Features
@@ -35,7 +35,11 @@ if (!fs.existsSync(cacheDir)) {
35
35
  fs.mkdirSync(cacheDir, { recursive: true });
36
36
  }
37
37
 
38
- const child = spawn(process.execPath, ["-e", `
38
+ const child = spawn(
39
+ process.execPath,
40
+ [
41
+ "-e",
42
+ `
39
43
  const fs = require("fs");
40
44
  const { execSync } = require("child_process");
41
45
 
@@ -54,7 +58,7 @@ const child = spawn(process.execPath, ["-e", `
54
58
 
55
59
  let latest = null;
56
60
  try {
57
- latest = execSync("npm view @vc-shell/vc-app-skill@alpha version", { encoding: "utf8", timeout: 10000, windowsHide: true }).trim();
61
+ latest = execSync("npm view @vc-shell/vc-app-skill@latest version", { encoding: "utf8", timeout: 10000, windowsHide: true }).trim();
58
62
  } catch (e) {}
59
63
 
60
64
  const result = {
@@ -65,10 +69,13 @@ const child = spawn(process.execPath, ["-e", `
65
69
  };
66
70
 
67
71
  fs.writeFileSync(cacheFile, JSON.stringify(result));
68
- `], {
69
- stdio: "ignore",
70
- windowsHide: true,
71
- detached: true
72
- });
72
+ `,
73
+ ],
74
+ {
75
+ stdio: "ignore",
76
+ windowsHide: true,
77
+ detached: true,
78
+ },
79
+ );
73
80
 
74
81
  child.unref();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vc-shell/vc-app-skill",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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.0.0
1
+ 2.0.2
@@ -1 +1 @@
1
- Synced from framework at commit be4d05add on 2026-04-22T12:54:33.589Z
1
+ Synced from framework at commit a5e644392 on 2026-04-27T09:03:25.568Z
@@ -17,7 +17,12 @@ The notification system receives `PushNotification` messages from the platform S
17
17
  ```
18
18
  PushNotification (SignalR)
19
19
  |
20
- v
20
+ ┌─────┴─────┐
21
+ Send SendSystemEvents
22
+ | |
23
+ | broadcastFilter?
24
+ | |
25
+ v v
21
26
  NotificationStore.ingest()
22
27
  |
23
28
  +---> history[] (persistent, loaded from API)
@@ -47,7 +52,9 @@ Returns the shared `NotificationStore` singleton. Resolution order:
47
52
  | `unreadCount` | `ComputedRef<number>` | Count of unread notifications in history |
48
53
  | `hasUnread` | `ComputedRef<boolean>` | Whether any unread notifications exist |
49
54
  | `registerType(type, config)` | `(string, NotificationTypeConfig) => void` | Register a notification type with toast/template config |
50
- | `ingest(message)` | `(PushNotification) => void` | Process an incoming notification (upsert, toast, notify subscribers) |
55
+ | `ingest(message, opts?)` | `(PushNotification, {broadcast?}?) => void` | Process an incoming notification; broadcast messages are filtered |
56
+ | `setBroadcastFilter(fn)` | `((PushNotification) => boolean) => void` | Set filter for broadcast messages (SendSystemEvents) |
57
+ | `clearBroadcastFilter()` | `() => void` | Remove the broadcast filter |
51
58
  | `markAsRead(message)` | `(PushNotification) => void` | Mark a single notification as read |
52
59
  | `markAllAsRead()` | `() => Promise<void>` | Optimistic mark-all-as-read with server sync and rollback on failure |
53
60
  | `loadHistory(take?)` | `(number?) => Promise<void>` | Load notification history from the API (default: 10) |
@@ -105,6 +112,35 @@ const notificationRef = useNotificationContext<IOrderNotification>();
105
112
  // notificationRef.value.orderId, notificationRef.value.title, etc.
106
113
  ```
107
114
 
115
+ ### `useBroadcastFilter()`
116
+
117
+ Controls which broadcast push notifications (`SendSystemEvents`) are accepted by the store. Targeted notifications (`Send`) are always accepted regardless of filter.
118
+
119
+ **File:** `composables/useBroadcastFilter.ts`
120
+
121
+ #### Returns
122
+
123
+ ```typescript
124
+ interface UseBroadcastFilterReturn {
125
+ setBroadcastFilter(fn: (msg: PushNotification) => boolean): void;
126
+ clearBroadcastFilter(): void;
127
+ }
128
+ ```
129
+
130
+ #### Example
131
+
132
+ ```typescript
133
+ import { useBroadcastFilter } from "@vc-shell/framework";
134
+
135
+ const { setBroadcastFilter, clearBroadcastFilter } = useBroadcastFilter();
136
+
137
+ // Accept only notifications for the current seller
138
+ setBroadcastFilter((msg) => msg.creator === currentSellerId);
139
+
140
+ // Remove filter (accept all broadcast messages)
141
+ clearBroadcastFilter();
142
+ ```
143
+
108
144
  ### Toast Controller
109
145
 
110
146
  Handles toast popup display based on `NotificationTypeConfig.toast` settings. Created internally by the store.
@@ -19,22 +19,20 @@ This centralized approach has several advantages:
19
19
 
20
20
  ### Blade Navigation
21
21
 
22
- | Key | Type | Description |
23
- | --------------------------- | -------------------------------------- | -------------------------------------------- |
24
- | `NavigationViewLocationKey` | `BladeVNode` | Current blade VNode location in navigation |
25
- | `BladeDescriptorKey` | `ComputedRef<BladeDescriptor>` | Current blade descriptor metadata |
26
- | `BladeBackButtonKey` | `Component \| undefined` | Custom back button component for a blade |
27
- | `BladeDataKey` | _(from blade-navigation types)_ | Data passed between parent/child blades |
28
- | `BladeContextKey` | `ComputedRef<Record<string, unknown>>` | Blade-exposed context for widgets/extensions |
29
- | `BladeRoutesKey` | `BladeRoutesRecord[]` | Registered blade routes |
30
- | `InternalRoutesKey` | `BladeRoutesRecord[]` | Internal framework routes |
22
+ | Key | Type | Description |
23
+ | -------------------- | -------------------------------------- | -------------------------------------------- |
24
+ | `BladeDescriptorKey` | `ComputedRef<BladeDescriptor>` | Current blade descriptor metadata |
25
+ | `BladeBackButtonKey` | `Component \| undefined` | Custom back button component for a blade |
26
+ | `BladeDataKey` | _(from blade-navigation types)_ | Data passed between parent/child blades |
27
+ | `BladeContextKey` | `ComputedRef<Record<string, unknown>>` | Blade-exposed context for widgets/extensions |
28
+ | `BladeRoutesKey` | `BladeRoutesRecord[]` | Registered blade routes |
29
+ | `InternalRoutesKey` | `BladeRoutesRecord[]` | Internal framework routes |
31
30
 
32
31
  ### Notifications
33
32
 
34
- | Key | Type | Description |
35
- | -------------------------- | ----------------------------------- | ------------------------------------------- |
36
- | `NotificationTemplatesKey` | `NotificationTemplateConstructor[]` | Registered notification template components |
37
- | `NotificationStoreKey` | `NotificationStore` | Shared notification store singleton |
33
+ | Key | Type | Description |
34
+ | ---------------------- | ------------------- | ----------------------------------- |
35
+ | `NotificationStoreKey` | `NotificationStore` | Shared notification store singleton |
38
36
 
39
37
  ### Services
40
38
 
@@ -42,7 +40,6 @@ This centralized approach has several advantages:
42
40
  | ------------------------------- | ----------------------------- | ------------------------------ |
43
41
  | `WidgetServiceKey` | `IWidgetService` | Widget registration and lookup |
44
42
  | `DashboardServiceKey` | `IDashboardService` | Dashboard widget management |
45
- | `GlobalSearchKey` | `GlobalSearchState` | Global search state |
46
43
  | `MenuServiceKey` | `MenuService` | Main navigation menu |
47
44
  | `SettingsMenuServiceKey` | `ISettingsMenuService` | Settings sidebar menu |
48
45
  | `AppBarWidgetServiceKey` | `IAppBarWidgetService` | App bar widget slots |
@@ -81,14 +78,23 @@ This centralized approach has several advantages:
81
78
 
82
79
  ### Legacy Aliases (Deprecated)
83
80
 
84
- | Deprecated | Use Instead |
85
- | ----------------------------- | --------------------------- |
86
- | `navigationViewLocation` | `NavigationViewLocationKey` |
87
- | `BladeDescriptor` | `BladeDescriptorKey` |
88
- | `NotificationTemplatesSymbol` | `NotificationTemplatesKey` |
89
- | `BLADE_BACK_BUTTON` | `BladeBackButtonKey` |
90
- | `TOOLBAR_SERVICE` | `ToolbarServiceKey` |
91
- | `EMBEDDED_MODE` | `EmbeddedModeKey` |
81
+ | Deprecated | Use Instead |
82
+ | ------------------- | -------------------- |
83
+ | `BladeDescriptor` | `BladeDescriptorKey` |
84
+ | `BLADE_BACK_BUTTON` | `BladeBackButtonKey` |
85
+ | `TOOLBAR_SERVICE` | `ToolbarServiceKey` |
86
+ | `EMBEDDED_MODE` | `EmbeddedModeKey` |
87
+
88
+ ### Removed Keys (No Replacement)
89
+
90
+ These keys existed in v1.2.3 but have been removed from the framework entirely. There is no drop-in symbol replacement.
91
+
92
+ | Removed | Notes |
93
+ | ----------------------------- | ------------------------------------------------------------------------------ |
94
+ | `navigationViewLocation` | Internal framework concern — no public replacement |
95
+ | `BladeInstance` | Use `useBlade()` composable, or `inject(BladeDescriptorKey)` |
96
+ | `NotificationTemplatesSymbol` | Template system replaced by `NotificationStoreKey` + `useBladeNotifications()` |
97
+ | `GlobalSearchKey` | Internal concern now — no public replacement |
92
98
 
93
99
  ## Usage Examples
94
100
 
@@ -0,0 +1,26 @@
1
+ # DashboardWidgetSkeleton
2
+
3
+ Internal placeholder card used by `GridstackDashboard` while remote modules are still loading. Mimics the shape of `DashboardWidgetCard` (header with icon + title, stats row, content lines) with a shimmer animation. Not exported from `@vc-shell/framework` — consumed only inside the dashboard organism.
4
+
5
+ ## When to Use
6
+
7
+ - You won't use this directly. `GridstackDashboard` renders one per `SkeletonItem` while `ModulesReadyKey` resolves to `false`.
8
+ - If you build a custom dashboard layout and want the same loading aesthetic, copy this file rather than importing it — the component is intentionally internal so the dashboard team can change its markup at any time.
9
+
10
+ ## Layout Contract
11
+
12
+ The skeleton has no props. Its parent positions it via inline `style` (`grid-column` / `grid-row`) inside a 12-column CSS grid. Card height/width is fully determined by the grid cell it occupies, so the skeleton stretches to fill its slot.
13
+
14
+ ## Accessibility
15
+
16
+ - The wrapper has `aria-hidden="true"` so screen readers ignore the visual placeholders. The parent grid carries `role="status"` + `aria-busy="true"` to announce loading state once.
17
+ - Shimmer animation is disabled when the user prefers reduced motion.
18
+
19
+ ## Design Tokens
20
+
21
+ Skeleton inherits dashboard card tokens where possible (`--dashboard-widget-card-background`, `--dashboard-widget-card-border-color`, `--dashboard-widget-card-border-radius`, `--dashboard-widget-card-shadow`) so its silhouette matches the real card. Shimmer colors are read from `--neutrals-100` / `--neutrals-200`.
22
+
23
+ ## Related
24
+
25
+ - [DraggableDashboard](./draggable-dashboard.docs.md) — owns the skeleton grid and decides when to render placeholders.
26
+ - [DashboardWidgetCard](../dashboard-widget-card/dashboard-widget-card.docs.md) — the real card whose shape skeletons imitate.
@@ -122,6 +122,8 @@ function resetLayout() {
122
122
  - **Widget registration**: Widgets must be registered via `useDashboard().registerWidget()` before the dashboard mounts. The component reads the widget registry and creates grid items for each.
123
123
  - **markRaw requirement**: Widget components must be wrapped in `markRaw()` when registering to prevent Vue from making them reactive (which would cause performance issues with the grid system).
124
124
  - **Responsive behavior**: On mobile viewports, widgets stack vertically in a single column. Drag-and-drop is disabled on touch devices for better usability.
125
+ - **Module-ready gate**: When the host injects `ModulesReadyKey` (provided by `@vc-shell/mf-host`'s `registerRemoteModules`), the grid is held back until remote modules finish installing. This prevents widgets from mounting before their owning module's `defineAppModule.install` has merged its locales into vue-i18n — which would render translation keys instead of translated strings. Hosts without Module Federation (or tests/Storybook) inject nothing, and a fallback `ref(true)` keeps behavior unchanged.
126
+ - **Loading skeletons**: While `ModulesReadyKey` is `false`, the dashboard renders a CSS-grid of pulsing placeholder cards instead of an empty container. Card sizes are restored from the last persisted layout in `localStorage` (so returning users see placeholders matching their real widgets); first-time visitors get a default layout of four 6×6 placeholders. Animations respect `prefers-reduced-motion`.
125
127
 
126
128
  ## Tips
127
129
 
@@ -63,13 +63,14 @@ const { canSave, isModified, setBaseline, revert, setFieldError, errorBag } = us
63
63
  closeConfirmMessage: () => t("MY_MODULE.ALERTS.CLOSE_CONFIRMATION"),
64
64
  });
65
65
 
66
- watch(
67
- item,
68
- () => {
69
- if (item.value) setBaseline();
70
- },
71
- { once: true },
72
- );
66
+ // Call setBaseline() AFTER data is loaded in onMounted.
67
+ // Do NOT use watch(item, ..., { once: true }) — it only detects ref
68
+ // replacement (item.value = newData), not in-place mutations
69
+ // (Object.assign(item.value, newData)).
70
+ onMounted(async () => {
71
+ await load(param.value);
72
+ setBaseline();
73
+ });
73
74
 
74
75
  async function handleSave() {
75
76
  await save(item.value);
@@ -77,6 +78,8 @@ async function handleSave() {
77
78
  }
78
79
  ```
79
80
 
81
+ **IMPORTANT — `setBaseline()` placement:** Always call `setBaseline()` AFTER the async load completes, inside `onMounted` or after the load function returns. A `watch(data, ..., { once: true })` watcher fails when the composable mutates data in-place via `Object.assign(data.value, loadedData)` instead of replacing the ref — the watcher never fires because the `.value` reference doesn't change.
82
+
80
83
  ## RULE 2: Remove onBeforeClose Entirely
81
84
 
82
85
  `useBladeForm` handles the unsaved-changes guard automatically via `autoBeforeClose` (defaults to `true`).
@@ -90,7 +90,45 @@ await closeSelf();
90
90
 
91
91
  If `closeBlade(index)` was used to close by index, refactor to close current blade only (or use explicit navigation logic) — do not keep index-based close calls.
92
92
 
93
- ## RULE 5: Consolidate multiple `useBlade()` calls per file
93
+ ## RULE 5: Remove `resolveBladeByName` pass name directly to `openBlade`
94
+
95
+ **BEFORE:**
96
+
97
+ ```typescript
98
+ const { openBlade, resolveBladeByName } = useBlade();
99
+ openBlade({ blade: resolveBladeByName("OrderDetails")!, param: orderId });
100
+ ```
101
+
102
+ **AFTER:**
103
+
104
+ ```typescript
105
+ const { openBlade } = useBlade();
106
+ openBlade({ name: "OrderDetails", param: orderId });
107
+ ```
108
+
109
+ Remove `resolveBladeByName` from the destructuring. The `blade` property in the openBlade argument is replaced by `name` (string).
110
+
111
+ ## RULE 6: Replace `onParentCall` with `callParent`
112
+
113
+ **BEFORE:**
114
+
115
+ ```typescript
116
+ const { onParentCall } = useBladeNavigation();
117
+ onParentCall({ method: "reload" });
118
+ onParentCall({ method: "updateItem", args: newItem });
119
+ ```
120
+
121
+ **AFTER:**
122
+
123
+ ```typescript
124
+ const { callParent } = useBlade();
125
+ await callParent("reload");
126
+ await callParent("updateItem", newItem);
127
+ ```
128
+
129
+ Note: `callParent` is async and returns a Promise. Add `await` if the result matters.
130
+
131
+ ## RULE 7: Consolidate multiple `useBlade()` calls per file
94
132
 
95
133
  Use one `useBlade()` destructuring per component/composable unless there is a strong technical reason.
96
134
 
@@ -111,7 +149,7 @@ const { openBlade, closeSelf, callParent, param } = useBlade();
111
149
 
112
150
  After migration:
113
151
 
114
- 1. Search and remove stale APIs: `useExternalWidgets`, `import moment`, `useFunctions(`, `closeBlade(`.
152
+ 1. Search and remove stale APIs: `useExternalWidgets`, `import moment`, `useFunctions(`, `closeBlade(`, `resolveBladeByName`, `onParentCall`.
115
153
  2. Search for duplicate `useBlade()` calls in the same file and consolidate.
116
154
  3. Run `npx vue-tsc --noEmit`.
117
155
  4. Run `yarn build`.
@@ -152,17 +152,28 @@ Toast options:
152
152
 
153
153
  ## RULE 4: Replace useNotifications in Blades
154
154
 
155
- Replace `useNotifications()` with `useBladeNotifications()`. Remove `setNotificationHandler`, `moduleNotifications` watch, and `notifyType` from `defineOptions`.
155
+ Replace `useNotifications()` with `useBladeNotifications()`. Remove `setNotificationHandler`, the entire `watch(moduleNotifications, ...)` block, manual toast management, and `notifyType` from `defineOptions`.
156
+
157
+ ### What to DELETE (toast management is now automatic via module config):
158
+
159
+ - The entire `watch(moduleNotifications/messages, ..., { deep: true })` block
160
+ - All `notification()` / `notification.update()` calls related to notification messages
161
+ - The `notificationId` ref used for toast tracking
162
+ - `setNotificationHandler(...)` calls
163
+ - `markAsRead()` calls inside toast `onClose` handlers
164
+ - `notifyType` from `defineOptions`/`defineBlade`
165
+
166
+ ### What to EXTRACT into `onMessage`:
167
+
168
+ - Only **data-reload logic** from the watch callback (e.g., `getTasks()`, `refreshData()`, `reload()`)
169
+ - Add `filter` if the callback had blade-specific conditions (e.g., `message.profileId === param.value`)
170
+
171
+ ### Simple case: `setNotificationHandler`
156
172
 
157
173
  **BEFORE:**
158
174
 
159
175
  ```typescript
160
- defineOptions({
161
- name: "MyBlade",
162
- notifyType: "MyDomainEvent",
163
- });
164
-
165
- const { markAsRead, setNotificationHandler, moduleNotifications } = useNotifications("MyDomainEvent");
176
+ const { markAsRead, setNotificationHandler } = useNotifications("MyDomainEvent");
166
177
 
167
178
  setNotificationHandler((message) => {
168
179
  if (message.title) {
@@ -173,12 +184,54 @@ setNotificationHandler((message) => {
173
184
  });
174
185
  }
175
186
  });
187
+ ```
188
+
189
+ **AFTER:**
190
+
191
+ ```typescript
192
+ useBladeNotifications({
193
+ types: ["MyDomainEvent"],
194
+ });
195
+ ```
196
+
197
+ ### Complex case: `watch` with toast management + data reload
198
+
199
+ **BEFORE:**
200
+
201
+ ```typescript
202
+ const { moduleNotifications, markAsRead } = useNotifications("ImportPushNotification");
203
+ const notificationId = ref();
176
204
 
177
- // or watch pattern:
178
205
  watch(
179
206
  moduleNotifications,
180
207
  (newVal) => {
181
- // manual toast management
208
+ (newVal as ImportPushNotification[]).forEach((message) => {
209
+ const messageContent = message.profileName ? `${message.profileName}: ${message.title}` : message.title;
210
+
211
+ // DATA RELOAD — extract this into onMessage
212
+ if (!importStarted.value && message.profileId === param.value) {
213
+ getTasks({ profileId: message.profileId, importJobId: message.jobId });
214
+ }
215
+
216
+ // TOAST MANAGEMENT — delete all of this (handled by module config)
217
+ if (!message.finished) {
218
+ if (!notificationId.value && messageContent) {
219
+ notificationId.value = notification(messageContent, { timeout: false });
220
+ } else {
221
+ notification.update(notificationId.value, { content: messageContent });
222
+ }
223
+ } else {
224
+ notification.update(notificationId.value, {
225
+ timeout: 5000,
226
+ content: messageContent,
227
+ type: message.title === "Import failed" ? "error" : "success",
228
+ onClose() {
229
+ markAsRead(message);
230
+ notificationId.value = undefined;
231
+ },
232
+ });
233
+ }
234
+ });
182
235
  },
183
236
  { deep: true },
184
237
  );
@@ -187,28 +240,23 @@ watch(
187
240
  **AFTER:**
188
241
 
189
242
  ```typescript
190
- defineOptions({
191
- name: "MyBlade",
192
- // notifyType removed
193
- });
194
-
195
- const { messages } = useBladeNotifications({
196
- types: ["MyDomainEvent"],
197
- onMessage: () => reloadData(),
198
- });
199
- ```
200
-
201
- For progress notifications:
202
-
203
- ```typescript
204
- // Module config: notifications: { MyProgressEvent: { toast: { mode: "progress" } } }
205
243
  useBladeNotifications({
206
- types: ["MyProgressEvent"],
207
- filter: (msg) => msg.entityId === param.value,
208
- onMessage: (msg) => refreshData(msg),
244
+ types: ["ImportPushNotification"],
245
+ filter: (msg) => (msg as ImportPushNotification).profileId === param.value,
246
+ onMessage: (msg) => {
247
+ if (!importStarted.value) {
248
+ getTasks({
249
+ profileId: (msg as ImportPushNotification).profileId,
250
+ importJobId: (msg as ImportPushNotification).jobId,
251
+ });
252
+ }
253
+ },
209
254
  });
255
+ // DELETE: notificationId ref, watch block, notification()/notification.update() calls
210
256
  ```
211
257
 
258
+ Toast appearance is configured in `defineAppModule({ notifications: { ImportPushNotification: { toast: { mode: "progress" } } } })`. The `markAsRead` is handled automatically.
259
+
212
260
  ## Verification
213
261
 
214
262
  After migration:
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: use-blade-migration
3
+ description: AI transformation rules for useBladeNavigation → useBlade, resolveBladeByName removal, onParentCall → callParent.
4
+ ---
5
+
6
+ # Blade Navigation Migration: useBladeNavigation → useBlade
7
+
8
+ Replace `useBladeNavigation()`, `useBladeContext()`, `resolveBladeByName()`, and `onParentCall()` with the unified `useBlade()` composable.
9
+
10
+ ## RULE 1: Replace useBladeNavigation / useBladeContext with useBlade
11
+
12
+ Rename the import and call site. Merge destructured properties from both into a single `useBlade()` call.
13
+
14
+ **BEFORE:**
15
+
16
+ ```typescript
17
+ import { useBladeNavigation } from "@vc-shell/framework";
18
+ const { openBlade, resolveBladeByName, onBeforeClose } = useBladeNavigation();
19
+ ```
20
+
21
+ **AFTER:**
22
+
23
+ ```typescript
24
+ import { useBlade } from "@vc-shell/framework";
25
+ const { openBlade, onBeforeClose } = useBlade();
26
+ ```
27
+
28
+ If both `useBladeNavigation()` and `useBladeContext()` are used, merge into one `useBlade()`:
29
+
30
+ **BEFORE:**
31
+
32
+ ```typescript
33
+ const blade = useBlade();
34
+ const { openBlade, onBeforeClose } = useBladeNavigation();
35
+ registerWidget(myWidget, blade?.value.id ?? "");
36
+ ```
37
+
38
+ **AFTER:**
39
+
40
+ ```typescript
41
+ const { id: bladeId, openBlade, onBeforeClose } = useBlade();
42
+ registerWidget(myWidget, bladeId.value);
43
+ ```
44
+
45
+ ## RULE 2: Remove resolveBladeByName
46
+
47
+ `resolveBladeByName` is no longer needed — pass the blade name directly to `openBlade`.
48
+
49
+ **BEFORE:**
50
+
51
+ ```typescript
52
+ openBlade({ blade: resolveBladeByName("OrderDetails")!, param: orderId });
53
+ ```
54
+
55
+ **AFTER:**
56
+
57
+ ```typescript
58
+ openBlade({ name: "OrderDetails", param: orderId });
59
+ ```
60
+
61
+ Also handle the pattern with options:
62
+
63
+ **BEFORE:**
64
+
65
+ ```typescript
66
+ openBlade({
67
+ blade: resolveBladeByName("ImportProcess")!,
68
+ param: profileId,
69
+ options: { title: profile.name, importJobId: jobId },
70
+ });
71
+ ```
72
+
73
+ **AFTER:**
74
+
75
+ ```typescript
76
+ openBlade({
77
+ name: "ImportProcess",
78
+ param: profileId,
79
+ options: { title: profile.name, importJobId: jobId },
80
+ });
81
+ ```
82
+
83
+ Remove the `resolveBladeByName` destructure from `useBlade()` since it's no longer used.
84
+
85
+ ## RULE 3: Replace onParentCall with callParent
86
+
87
+ **BEFORE:**
88
+
89
+ ```typescript
90
+ const { onParentCall } = useBladeNavigation();
91
+ onParentCall({ method: "reload" });
92
+ onParentCall({ method: "updateItem", args: newItem });
93
+ ```
94
+
95
+ **AFTER:**
96
+
97
+ ```typescript
98
+ const { callParent } = useBlade();
99
+ await callParent("reload");
100
+ await callParent("updateItem", newItem);
101
+ ```
102
+
103
+ Note: `callParent` is async and returns a Promise.
104
+
105
+ ## RULE 4: Invert onBeforeClose Boolean
106
+
107
+ The guard semantics are **inverted** in the new API:
108
+
109
+ | Legacy | New |
110
+ | ------------------------------ | ----------------------------- |
111
+ | `return false` → prevent close | `return true` → prevent close |
112
+ | `return true` → allow close | `return false` → allow close |
113
+
114
+ **BEFORE:**
115
+
116
+ ```typescript
117
+ onBeforeClose(async () => {
118
+ if (isModified.value) {
119
+ return await showConfirmation(msg); // true = allow, false = prevent
120
+ }
121
+ });
122
+ ```
123
+
124
+ **AFTER:**
125
+
126
+ ```typescript
127
+ onBeforeClose(async () => {
128
+ if (isModified.value) {
129
+ return !(await showConfirmation(msg)); // true = prevent, false = allow
130
+ }
131
+ });
132
+ ```
133
+
134
+ If `useBladeForm()` is used, `onBeforeClose` can be removed entirely — `useBladeForm` handles it automatically.
135
+
136
+ ## RULE 5: Clean Up Unused Imports
137
+
138
+ After applying all rules, remove:
139
+
140
+ - `useBladeNavigation` import
141
+ - `useBladeContext` import
142
+ - `resolveBladeByName` import (if it was imported separately)
143
+ - Any unused destructured variables
144
+
145
+ ## Verification
146
+
147
+ After migration:
148
+
149
+ 1. Run `npx vue-tsc --noEmit` to verify no TypeScript errors
150
+ 2. Confirm `openBlade` calls navigate correctly
151
+ 3. Confirm blade close confirmation dialogs still appear when data is modified
152
+ 4. Confirm no remaining imports of `useBladeNavigation`, `useBladeContext`, or `resolveBladeByName`
153
+ 5. Confirm no remaining `onParentCall` calls
package/runtime/vc-app.md CHANGED
@@ -1600,19 +1600,19 @@ Parse the "Manual Migration Required" section. Extract each topic heading and th
1600
1600
 
1601
1601
  Map topic headings to migration prompt files and pattern files:
1602
1602
 
1603
- | Report Heading contains (or equals transform name) | Canonical topic name | Migration Prompt | Pattern |
1604
- | -------------------------------------------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------- |
1605
- | Widget / widgets-migration | `widgets-migration` | `{KNOWLEDGE_BASE}/migration-prompts/widgets-migration.md` | `{KNOWLEDGE_BASE}/patterns/blade-widget.md` |
1606
- | Form Management / useBladeForm / use-blade-form | `use-blade-form` | `{KNOWLEDGE_BASE}/migration-prompts/blade-form-migration.md` | `{KNOWLEDGE_BASE}/patterns/form-validation.md` |
1607
- | Injection Key / remove-deprecated-aliases | `remove-deprecated-aliases` | `{KNOWLEDGE_BASE}/migration-prompts/blade-props-migration.md` | `{KNOWLEDGE_BASE}/patterns/blade-navigation.md` |
1608
- | NSwag / DTO / Clone-then-mutate / nswag-class-to-interface | `nswag-class-to-interface` | `{KNOWLEDGE_BASE}/migration-prompts/nswag-migration.md` | — |
1609
- | Reusable Blade Components / blade-props-simplification | `blade-props-simplification` | `{KNOWLEDGE_BASE}/migration-prompts/blade-props-migration.md` | `{KNOWLEDGE_BASE}/patterns/child-blade-flow.md` |
1610
- | Notification / notification-migration | `notification-migration` | `{KNOWLEDGE_BASE}/migration-prompts/notifications-migration.md` | `{KNOWLEDGE_BASE}/patterns/signalr-notifications.md` |
1611
- | VcTable / DataTable / vctable-audit | `vctable-audit` | `{KNOWLEDGE_BASE}/migration-prompts/datatable-migration.md` | `{KNOWLEDGE_BASE}/patterns/datatable-pattern.md` |
1612
- | Icon / material- / bi- / fa- / icon-audit | `icon-audit` | `{KNOWLEDGE_BASE}/migration-prompts/icon-migration.md` | — |
1613
- | Assets API / useAssets / useAssetsManager / use-assets-migration | `use-assets-migration` | `{KNOWLEDGE_BASE}/migration-prompts/use-assets-migration.md` | `{KNOWLEDGE_BASE}/patterns/assets-management.md` |
1614
- | Manual Migration Audit / useExternalWidgets / moment / useFunctions / manual-migration-audit | `manual-migration-audit` | `{KNOWLEDGE_BASE}/migration-prompts/manual-migration-audit.md` | — |
1615
- | Pagination / useDataTablePagination / use-data-table-pagination-audit | `use-data-table-pagination-audit` | `{KNOWLEDGE_BASE}/migration-prompts/use-data-table-pagination-migration.md` | — |
1603
+ | Report Heading contains (or equals transform name) | Canonical topic name | Migration Prompt | Pattern |
1604
+ | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------- |
1605
+ | Widget / widgets-migration | `widgets-migration` | `{KNOWLEDGE_BASE}/migration-prompts/widgets-migration.md` | `{KNOWLEDGE_BASE}/patterns/blade-widget.md` |
1606
+ | Form Management / useBladeForm / use-blade-form | `use-blade-form` | `{KNOWLEDGE_BASE}/migration-prompts/blade-form-migration.md` | `{KNOWLEDGE_BASE}/patterns/form-validation.md` |
1607
+ | Injection Key / remove-deprecated-aliases | `remove-deprecated-aliases` | `{KNOWLEDGE_BASE}/migration-prompts/blade-props-migration.md` | `{KNOWLEDGE_BASE}/patterns/blade-navigation.md` |
1608
+ | NSwag / DTO / Clone-then-mutate / nswag-class-to-interface | `nswag-class-to-interface` | `{KNOWLEDGE_BASE}/migration-prompts/nswag-migration.md` | — |
1609
+ | Reusable Blade Components / blade-props-simplification | `blade-props-simplification` | `{KNOWLEDGE_BASE}/migration-prompts/blade-props-migration.md` | `{KNOWLEDGE_BASE}/patterns/child-blade-flow.md` |
1610
+ | Notification / notification-migration | `notification-migration` | `{KNOWLEDGE_BASE}/migration-prompts/notifications-migration.md` | `{KNOWLEDGE_BASE}/patterns/signalr-notifications.md` |
1611
+ | VcTable / DataTable / vctable-audit | `vctable-audit` | `{KNOWLEDGE_BASE}/migration-prompts/datatable-migration.md` | `{KNOWLEDGE_BASE}/patterns/datatable-pattern.md` |
1612
+ | Icon / material- / bi- / fa- / icon-audit | `icon-audit` | `{KNOWLEDGE_BASE}/migration-prompts/icon-migration.md` | — |
1613
+ | Assets API / useAssets / useAssetsManager / use-assets-migration | `use-assets-migration` | `{KNOWLEDGE_BASE}/migration-prompts/use-assets-migration.md` | `{KNOWLEDGE_BASE}/patterns/assets-management.md` |
1614
+ | Manual Migration Audit / useExternalWidgets / moment / useFunctions / resolveBladeByName / onParentCall / manual-migration-audit | `manual-migration-audit` | `{KNOWLEDGE_BASE}/migration-prompts/manual-migration-audit.md` | — |
1615
+ | Pagination / useDataTablePagination / use-data-table-pagination-audit | `use-data-table-pagination-audit` | `{KNOWLEDGE_BASE}/migration-prompts/use-data-table-pagination-migration.md` | — |
1616
1616
 
1617
1617
  Build the `topics` array for the migration-agent using the canonical topic names above.
1618
1618