@vc-shell/vc-app-skill 2.0.0-alpha.24 → 2.0.0-alpha.25

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,3 +1,7 @@
1
+ # [2.0.0-alpha.25](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.24...v2.0.0-alpha.25) (2026-03-25)
2
+
3
+ **Note:** Version bump only for package @vc-shell/vc-app-skill
4
+
1
5
  # [2.0.0-alpha.24](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.23...v2.0.0-alpha.24) (2026-03-25)
2
6
 
3
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vc-shell/vc-app-skill",
3
- "version": "2.0.0-alpha.24",
3
+ "version": "2.0.0-alpha.25",
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-alpha.24
1
+ 2.0.0-alpha.25
@@ -1 +1 @@
1
- Synced from framework at commit c001cdf1a on 2026-03-25T10:46:45.153Z
1
+ Synced from framework at commit 99a2f022b on 2026-03-25T11:57:14.169Z
@@ -1,15 +1,19 @@
1
- # useBladeWidgets
1
+ # useBladeWidgets / useWidgetTrigger
2
2
 
3
- Declarative headless widget registration for blades. Widgets are automatically registered with the WidgetService on mount and cleaned up on unmount, following the Vue component lifecycle. This composable is the recommended way to add sidebar counter widgets, action buttons, and status indicators to a blade without creating separate Vue component files for each widget.
3
+ Two composables for the widget system one for the **blade side**, one for the **widget side**.
4
4
 
5
- Headless widgets are defined as plain configuration objects with reactive refs for dynamic values like badge counts and loading states. The WidgetService renders them in the blade sidebar.
5
+ | Composable | Called from | Purpose |
6
+ |---|---|---|
7
+ | `useBladeWidgets` | Blade component | Register headless widgets + get `refresh()` / `refreshAll()` |
8
+ | `useWidgetTrigger` | External widget component | Register trigger callbacks (`onRefresh`, `onClick`) via provide/inject |
9
+
10
+ Headless widgets are defined as plain configuration objects with reactive refs for dynamic values like badge counts and loading states. External component-based widgets use `useWidgetTrigger` to register their refresh callbacks so the hosting blade can trigger them.
6
11
 
7
12
  ## When to Use
8
13
 
9
- - Register sidebar widgets (counters, action buttons) for a blade without creating Vue components
10
- - Refresh widget data programmatically after blade operations (save, delete, status change)
11
- - Show/hide widgets conditionally based on blade state (e.g., only show after entity is saved)
12
- - When NOT to use: for widgets that need their own template or complex UI (use a full widget component instead)
14
+ - **`useBladeWidgets`**: Register sidebar widgets (counters, action buttons) for a blade without creating Vue components. Refresh widget data after blade operations (save, delete).
15
+ - **`useWidgetTrigger`**: Inside an external widget component (registered via `registerExternalWidget`) to register `onRefresh` / `onClick` callbacks. The blade can then call `refresh(widgetId)` or `refreshAll()` to trigger them.
16
+ - When NOT to use `useBladeWidgets`: for widgets that need their own template or complex UI (use `registerExternalWidget` + `useWidgetTrigger` instead).
13
17
 
14
18
  ## Basic Usage
15
19
 
@@ -70,6 +74,121 @@ refreshAll();
70
74
  | `refresh` | `(widgetId: string) => void` | Trigger `onRefresh` on a specific widget |
71
75
  | `refreshAll` | `() => void` | Trigger `onRefresh` on all widgets that have one |
72
76
 
77
+ ## useWidgetTrigger
78
+
79
+ Widget-side composable for external component-based widgets. Registers a trigger contract (`onRefresh`, `onClick`, `badge`) via provide/inject — no props, IDs, or service knowledge required.
80
+
81
+ ### Basic Usage
82
+
83
+ ```typescript
84
+ import { useWidgetTrigger } from '@vc-shell/framework';
85
+
86
+ // Inside an external widget component:
87
+ useWidgetTrigger({ onRefresh: loadData });
88
+ ```
89
+
90
+ ### IWidgetTrigger
91
+
92
+ | Field | Type | Required | Description |
93
+ |---|---|---|---|
94
+ | `icon` | `string` | No | Lucide icon name for dropdown rendering |
95
+ | `title` | `string` | No | Display title (fallback: widget's title) |
96
+ | `badge` | `Ref<number \| string>` | No | Reactive badge value |
97
+ | `onClick` | `() => void` | No | Handler called when widget is clicked in dropdown |
98
+ | `onRefresh` | `() => void \| Promise<void>` | No | Handler called to refresh widget data |
99
+ | `disabled` | `Ref<boolean> \| boolean` | No | Disabled state |
100
+
101
+ ### How It Works
102
+
103
+ 1. `WidgetContainer` wraps each component-based widget in a `WidgetScope` provider
104
+ 2. `WidgetScope` provides a `setTrigger` function scoped to the specific widget ID and blade ID
105
+ 3. `useWidgetTrigger` injects this scope and calls `setTrigger` — no props or IDs needed
106
+ 4. When the blade calls `refresh(widgetId)` or `refreshAll()`, the registered `onRefresh` is invoked
107
+
108
+ ## Recipe: External Widget with Refresh
109
+
110
+ A complete example of an external widget that shows an unread message count and supports refresh from the blade:
111
+
112
+ **1. Register the external widget (module index.ts):**
113
+
114
+ ```typescript
115
+ import { createAppModule, registerExternalWidget, IBladeInstance } from "@vc-shell/framework";
116
+ import { markRaw } from "vue";
117
+ import { MessageWidget } from "./components/widgets";
118
+
119
+ registerExternalWidget({
120
+ id: "MessageWidget",
121
+ component: markRaw(MessageWidget),
122
+ targetBlades: ["ProductDetails", "OrderDetails"],
123
+ isVisible: (blade?: IBladeInstance) => !!blade?.param,
124
+ });
125
+ ```
126
+
127
+ **2. Widget component (message-widget.vue):**
128
+
129
+ ```vue
130
+ <template>
131
+ <VcWidget
132
+ v-loading:500="loading"
133
+ :title="$t('MESSENGER.WIDGET.TITLE')"
134
+ icon="lucide-message-circle"
135
+ :value="messageCount"
136
+ @click="openMessageBlade"
137
+ />
138
+ </template>
139
+
140
+ <script setup lang="ts">
141
+ import { ref, computed, onMounted } from "vue";
142
+ import {
143
+ loading as vLoading,
144
+ useBlade,
145
+ injectBladeContext,
146
+ useWidgetTrigger,
147
+ VcWidget,
148
+ } from "@vc-shell/framework";
149
+
150
+ const ctx = injectBladeContext();
151
+ const entityId = computed(() => (ctx.value.item as { id?: string })?.id ?? "");
152
+
153
+ const messageCount = ref(0);
154
+ const loading = ref(false);
155
+
156
+ const loadData = async () => {
157
+ loading.value = true;
158
+ try {
159
+ messageCount.value = await api.getUnreadCount(entityId.value);
160
+ } finally {
161
+ loading.value = false;
162
+ }
163
+ };
164
+
165
+ // Register refresh callback — blade can call refreshAll() after save
166
+ useWidgetTrigger({ onRefresh: loadData });
167
+
168
+ onMounted(() => {
169
+ if (entityId.value) loadData();
170
+ });
171
+ </script>
172
+ ```
173
+
174
+ **3. Blade refreshes widgets after save:**
175
+
176
+ ```vue
177
+ <script setup lang="ts">
178
+ import { useBladeWidgets } from "@vc-shell/framework";
179
+
180
+ // Empty array — blade doesn't register headless widgets,
181
+ // but gets refresh/refreshAll for external widgets
182
+ const { refresh, refreshAll } = useBladeWidgets([]);
183
+
184
+ async function save() {
185
+ await api.saveProduct(product.value);
186
+ refreshAll(); // refresh all widgets (including MessageWidget)
187
+ // or: refresh("MessageWidget"); // refresh a specific widget by ID
188
+ }
189
+ </script>
190
+ ```
191
+
73
192
  ## Recipe: Product Detail Blade with Multiple Widgets
74
193
 
75
194
  ```vue
@@ -126,10 +245,15 @@ async function save() {
126
245
 
127
246
  ## Prerequisites
128
247
 
248
+ **`useBladeWidgets`**:
129
249
  - Must be called inside a blade component rendered by `VcBladeSlot` (requires `BladeDescriptorKey` injection).
130
250
  - `WidgetService` must be provided in the component tree (automatically available in vc-shell apps).
131
251
  - Calling outside a blade context throws an error with a descriptive message.
132
252
 
253
+ **`useWidgetTrigger`**:
254
+ - Must be called inside a widget component rendered by `WidgetContainer` (requires `WidgetScopeKey` injection).
255
+ - If called outside a widget scope, logs a warning and does nothing (does not throw).
256
+
133
257
  ## Details
134
258
 
135
259
  - **Lifecycle management**: Widgets are registered in `onMounted` and unregistered in `onUnmounted`. This ensures the WidgetService always reflects the currently visible blades.
@@ -143,8 +267,39 @@ async function save() {
143
267
  - Keep widget IDs unique within a blade. Duplicate IDs will overwrite previous registrations.
144
268
  - Combine with `defineBladeContext` to expose blade entity data that widget components (non-headless) can consume via `injectBladeContext`.
145
269
 
270
+ ## Common Mistakes
271
+
272
+ ### Calling useWidgetTrigger outside WidgetContainer scope
273
+
274
+ ```typescript
275
+ // Wrong — called in a standalone component, not rendered inside a blade widget slot
276
+ export default defineComponent({
277
+ setup() {
278
+ useWidgetTrigger({ onRefresh: loadData }); // ⚠️ Logs warning, trigger not registered
279
+ },
280
+ });
281
+ ```
282
+
283
+ ```typescript
284
+ // Correct — called inside a widget component registered via registerExternalWidget
285
+ // and rendered by WidgetContainer within a blade
286
+ useWidgetTrigger({ onRefresh: loadData }); // ✓ WidgetScope provides context
287
+ ```
288
+
289
+ ### Forgetting to pass empty array to useBladeWidgets for refresh-only usage
290
+
291
+ ```typescript
292
+ // Wrong — useBladeWidgets requires an array argument
293
+ const { refreshAll } = useBladeWidgets(); // TS error
294
+
295
+ // Correct — pass empty array when you only need refresh/refreshAll
296
+ const { refreshAll } = useBladeWidgets([]);
297
+ ```
298
+
146
299
  ## Related
147
300
 
148
- - `defineBladeContext` -- expose blade data to widgets
149
- - `WidgetService` in `@core/services/widget-service`
301
+ - `defineBladeContext` / `injectBladeContext` -- expose/consume blade data in external widgets
302
+ - `registerExternalWidget` -- register a component-based widget globally for target blades
303
+ - `WidgetService` in `@core/services/widget-service` -- underlying service
304
+ - `WidgetScope` in `vc-blade/_internal/widgets/WidgetScope.vue` -- provides `WidgetScopeKey` to widget components
150
305
  - `VcBladeSlot` -- the blade wrapper that provides `BladeDescriptorKey`
@@ -9,7 +9,7 @@ Also exports `provideWidgetService()` for framework-level initialization and `re
9
9
  - When you need low-level access to the widget service (register, unregister, query widgets)
10
10
  - When building framework infrastructure that manages widget lifecycles
11
11
  - When pre-registering widgets from a module's `install()` function before the component tree exists
12
- - When NOT to use: for typical blade widget work, prefer `useBladeWidgets()` which handles lifecycle automatically. For widget-side logic (badge, refresh), use `useWidget()`.
12
+ - When NOT to use: for typical blade widget work, prefer `useBladeWidgets()` which handles lifecycle automatically. For widget-side logic (badge, refresh), use headless widgets.
13
13
 
14
14
  ## Quick Start
15
15
 
@@ -46,7 +46,7 @@ None.
46
46
  | `setActiveWidget` | `(args) => void` | Sets a widget as active with its exposed instance. |
47
47
  | `updateActiveWidget` | `() => void` | Triggers the active widget's update function. Deprecated -- use headless widgets instead. |
48
48
  | `isWidgetRegistered` | `(id: string) => boolean` | Checks if a widget ID exists in any blade's registry. |
49
- | `updateWidget` | `(args) => void` | Updates properties of a registered widget (trigger, badge, etc.). Used by `useWidget().setTrigger()`. |
49
+ | `updateWidget` | `(args) => void` | Updates properties of a registered widget (trigger, badge, etc.). |
50
50
  | `resolveWidgetProps` | `(widget, bladeData) => Record<string, unknown>` | Resolves widget props from blade data. Deprecated. |
51
51
  | `getExternalWidgetsForBlade` | `(bladeId: string) => IExternalWidgetRegistration[]` | Gets external widgets that target a specific blade (registered by other modules). |
52
52
  | `getAllExternalWidgets` | `() => IExternalWidgetRegistration[]` | Gets all registered external widgets across all modules. |
@@ -62,7 +62,7 @@ This centralized approach has several advantages:
62
62
 
63
63
  | Key | Type | Description |
64
64
  |-----|------|-------------|
65
- | `WidgetIdKey` | `string` | Widget identity (provided by WidgetProvider) |
65
+ | `WidgetScopeKey` | `IWidgetScope` | Widget scope (provided by WidgetContainer for component-based widgets via `WidgetScope.vue`) |
66
66
  | `AppRootElementKey` | `Ref<HTMLElement \| undefined>` | App root element for scoped Teleport |
67
67
  | `EmbeddedModeKey` | `boolean` | Whether the app runs in embedded mode |
68
68
  | `ShellIndicatorsKey` | `ComputedRef<boolean>` | Unread indicator state for sidebar |
@@ -1,159 +0,0 @@
1
- # useWidget
2
-
3
- Widget-side composable that auto-discovers widget identity from `WidgetProvider` and provides a `setTrigger()` method to register refresh/badge contracts with the widget service. This composable is the counterpart to the blade-side `useWidgets()` / `useBladeWidgets()` -- while those manage widget registration from the blade's perspective, `useWidget()` is called from inside the widget component itself to communicate its state (badge count, icon, refresh callback) back to the widget system.
4
-
5
- **Deprecated**: Prefer headless widgets via `useBladeWidgets()` instead. This composable is only needed for legacy SFC widgets that register trigger contracts manually.
6
-
7
- ## When to Use
8
-
9
- - Inside a widget component wrapped by `WidgetProvider` to register badge counts, refresh callbacks, or click handlers
10
- - When building a custom blade widget that needs to communicate its state back to the parent blade's widget panel
11
- - When NOT to use: outside a `WidgetProvider` context (will throw). For managing widgets from the blade side, use `useWidgets()` or `useBladeWidgets()` instead. For new widgets, prefer the headless widget pattern.
12
-
13
- ## Quick Start
14
-
15
- ```vue
16
- <script setup lang="ts">
17
- import { useWidget } from '@vc-shell/framework';
18
- import { ref, computed, onMounted } from 'vue';
19
-
20
- const { widgetId, setTrigger } = useWidget();
21
-
22
- const items = ref<unknown[]>([]);
23
- const unreadCount = computed(() => items.value.filter((i) => !i.isRead).length);
24
-
25
- async function fetchData() {
26
- items.value = await api.getWidgetItems();
27
- }
28
-
29
- // Register the trigger contract: badge + refresh callback
30
- setTrigger({
31
- badge: unreadCount,
32
- icon: 'fas fa-inbox',
33
- title: 'Messages',
34
- });
35
-
36
- onMounted(fetchData);
37
- </script>
38
-
39
- <template>
40
- <div class="tw-p-4">
41
- <p class="tw-text-sm tw-text-gray-500">Widget ID: {{ widgetId }}</p>
42
- <ul>
43
- <li v-for="item in items" :key="item.id">{{ item.title }}</li>
44
- </ul>
45
- </div>
46
- </template>
47
- ```
48
-
49
- ## API
50
-
51
- ### Parameters
52
-
53
- None. Identity is auto-discovered from the `WidgetProvider` injection context. The composable injects `WidgetIdKey` for the widget ID, `WidgetServiceKey` for the service, and `BladeDescriptorKey` for the parent blade context.
54
-
55
- ### Returns
56
-
57
- | Property | Type | Description |
58
- |----------|------|-------------|
59
- | `widgetId` | `string` | The widget's unique ID, injected by `WidgetProvider`. |
60
- | `setTrigger` | `(trigger: IWidgetTrigger) => void` | Registers a trigger contract (badge, icon, title, callbacks) for this widget. Can be called multiple times to update the trigger. |
61
-
62
- ### IWidgetTrigger
63
-
64
- | Property | Type | Description |
65
- |----------|------|-------------|
66
- | `icon` | `string?` | Lucide or FontAwesome icon name for the widget's display in the dropdown/panel. |
67
- | `title` | `string?` | Display title for the widget. Falls back to `IWidget.title` if not set. |
68
- | `badge` | `Ref<number \| string> \| ComputedRef<number \| string>?` | Reactive badge value displayed on the widget button/tab. Pass a computed ref for automatic updates. |
69
-
70
- ## How It Works
71
-
72
- 1. **Identity discovery**: On creation, the composable injects `WidgetIdKey` (provided by `WidgetProvider`) and `WidgetServiceKey` (provided by `provideWidgetService()`). It also injects `BladeDescriptorKey` to know which blade this widget belongs to.
73
-
74
- 2. **Trigger registration**: When you call `setTrigger(trigger)`, the composable calls `widgetService.updateWidget({ id: widgetId, bladeId, widget: { trigger } })`. This stores the trigger contract in the widget service, where the blade's widget panel can read it.
75
-
76
- 3. **Reactive badges**: Because `badge` accepts a `Ref` or `ComputedRef`, the widget panel automatically updates whenever the badge value changes. There is no need to call `setTrigger` again after the initial registration.
77
-
78
- ## Recipe: Widget with Live Badge Count
79
-
80
- ```vue
81
- <script setup lang="ts">
82
- import { useWidget } from '@vc-shell/framework';
83
- import { ref, computed, onMounted } from 'vue';
84
-
85
- const { setTrigger } = useWidget();
86
- const pendingOrders = ref<Order[]>([]);
87
-
88
- async function loadPendingOrders() {
89
- pendingOrders.value = await orderApi.getPending();
90
- }
91
-
92
- // Badge shows the count of pending orders
93
- setTrigger({
94
- badge: computed(() => pendingOrders.value.length),
95
- icon: 'fas fa-clock',
96
- title: 'Pending Orders',
97
- });
98
-
99
- onMounted(loadPendingOrders);
100
-
101
- // Optionally refresh periodically
102
- const interval = setInterval(loadPendingOrders, 30_000);
103
- onUnmounted(() => clearInterval(interval));
104
- </script>
105
-
106
- <template>
107
- <div class="tw-space-y-2">
108
- <div v-for="order in pendingOrders" :key="order.id" class="tw-p-2 tw-border tw-rounded">
109
- <p class="tw-font-medium">{{ order.number }}</p>
110
- <p class="tw-text-sm tw-text-gray-500">{{ order.status }}</p>
111
- </div>
112
- <p v-if="!pendingOrders.length" class="tw-text-gray-400">No pending orders</p>
113
- </div>
114
- </template>
115
- ```
116
-
117
- ## Recipe: Widget That Updates on Blade Data Change
118
-
119
- ```vue
120
- <script setup lang="ts">
121
- import { useWidget } from '@vc-shell/framework';
122
- import { inject, watch, ref, computed } from 'vue';
123
-
124
- const { setTrigger } = useWidget();
125
-
126
- // Inject blade data from parent
127
- const bladeData = inject('bladeData') as Ref<{ orderId: string }>;
128
- const comments = ref<Comment[]>([]);
129
-
130
- async function loadComments(orderId: string) {
131
- comments.value = await commentApi.getByOrder(orderId);
132
- }
133
-
134
- // Reload when blade data changes
135
- watch(() => bladeData.value.orderId, (id) => {
136
- if (id) loadComments(id);
137
- }, { immediate: true });
138
-
139
- setTrigger({
140
- badge: computed(() => comments.value.length),
141
- icon: 'fas fa-comments',
142
- title: 'Comments',
143
- });
144
- </script>
145
- ```
146
-
147
- ## Tips
148
-
149
- - **Must be inside `WidgetProvider`.** This composable relies on `WidgetIdKey` being injected by `WidgetProvider`. If called outside that context, it throws an `InjectionError`. The `WidgetProvider` component is automatically rendered by the blade widget system.
150
- - **`setTrigger` can be called multiple times.** Each call replaces the previous trigger contract. However, since `badge` is reactive, you typically only need to call `setTrigger` once during setup.
151
- - **Badge accepts both numbers and strings.** You can use `computed(() => 3)` for a numeric badge or `computed(() => '!')` for a text indicator.
152
- - **Prefer headless widgets for new code.** The `useBladeWidgets()` composable supports a headless pattern where widgets are defined declaratively without a separate SFC. This is simpler and avoids the WidgetProvider ceremony.
153
-
154
- ## Related
155
-
156
- - [useWidgets](../useWidgets/useWidgets.docs.md) -- blade-level widget service access
157
- - [useBladeWidgets](../useBladeWidgets/) -- lifecycle-managed widget registration for blades (preferred API for new code)
158
- - `WidgetProvider` -- the component that provides the `WidgetIdKey` injection
159
- - `framework/injection-keys.ts` -- defines `WidgetIdKey` and `WidgetServiceKey`