@vc-shell/vc-app-skill 2.0.0-alpha.29 → 2.0.0-alpha.31

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.
@@ -1,305 +0,0 @@
1
- # useBladeWidgets / useWidgetTrigger
2
-
3
- Two composables for the widget system — one for the **blade side**, one for the **widget side**.
4
-
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.
11
-
12
- ## When to Use
13
-
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).
17
-
18
- ## Basic Usage
19
-
20
- ```typescript
21
- import { useBladeWidgets } from '@vc-shell/framework';
22
-
23
- const { refreshAll } = useBladeWidgets([
24
- {
25
- id: 'OffersWidget',
26
- icon: 'lucide-tag',
27
- title: 'OFFERS.TITLE',
28
- badge: offersCount,
29
- loading: offersLoading,
30
- onClick: () => openBlade({ name: 'OffersList' }),
31
- onRefresh: () => reloadOffers(),
32
- },
33
- {
34
- id: 'ReviewsWidget',
35
- icon: 'lucide-star',
36
- title: 'REVIEWS.TITLE',
37
- badge: reviewsCount,
38
- isVisible: computed(() => !!item.value?.id),
39
- onClick: () => openBlade({ name: 'ReviewsList' }),
40
- },
41
- ]);
42
-
43
- // After a save, refresh all widget data
44
- await saveEntity();
45
- refreshAll();
46
- ```
47
-
48
- ## API
49
-
50
- ### Parameters
51
-
52
- | Parameter | Type | Required | Description |
53
- |---|---|---|---|
54
- | `widgets` | `HeadlessWidgetDeclaration[]` | Yes | Array of widget declarations |
55
-
56
- ### HeadlessWidgetDeclaration
57
-
58
- | Field | Type | Required | Description |
59
- |---|---|---|---|
60
- | `id` | `string` | Yes | Unique widget identifier |
61
- | `icon` | `string` | Yes | Icon name (e.g., `"lucide-tag"`) |
62
- | `title` | `string` | Yes | i18n key or display title |
63
- | `badge` | `Ref<number \| string>` | No | Badge counter value |
64
- | `loading` | `Ref<boolean>` | No | Show loading indicator |
65
- | `disabled` | `Ref<boolean> \| boolean` | No | Disable the widget |
66
- | `isVisible` | `ComputedRef<boolean> \| boolean` | No | Toggle visibility |
67
- | `onClick` | `() => void` | No | Action when widget is clicked |
68
- | `onRefresh` | `() => void \| Promise<void>` | No | Called by `refresh(id)` or `refreshAll()` |
69
-
70
- ### Returns
71
-
72
- | Property | Type | Description |
73
- |---|---|---|
74
- | `refresh` | `(widgetId: string) => void` | Trigger `onRefresh` on a specific widget |
75
- | `refreshAll` | `() => void` | Trigger `onRefresh` on all widgets that have one |
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, BladeDescriptor } 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?: BladeDescriptor) => !!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
-
192
- ## Recipe: Product Detail Blade with Multiple Widgets
193
-
194
- ```vue
195
- <script setup lang="ts">
196
- import { ref, computed } from "vue";
197
- import { useBladeWidgets, defineBladeContext } from "@vc-shell/framework";
198
-
199
- const product = ref({ id: "prod-1", name: "Widget A" });
200
- const offersCount = ref(0);
201
- const reviewsCount = ref(0);
202
- const offersLoading = ref(false);
203
-
204
- // Expose product data to widgets
205
- defineBladeContext(computed(() => ({ id: product.value?.id })));
206
-
207
- async function reloadOffers() {
208
- offersLoading.value = true;
209
- try {
210
- const result = await api.searchOffers({ productId: product.value.id });
211
- offersCount.value = result.totalCount;
212
- } finally {
213
- offersLoading.value = false;
214
- }
215
- }
216
-
217
- const { refreshAll } = useBladeWidgets([
218
- {
219
- id: "OffersWidget",
220
- icon: "lucide-tag",
221
- title: "PRODUCT.WIDGETS.OFFERS",
222
- badge: offersCount,
223
- loading: offersLoading,
224
- isVisible: computed(() => !!product.value?.id),
225
- onClick: () => openBlade({ name: "OffersList" }),
226
- onRefresh: reloadOffers,
227
- },
228
- {
229
- id: "ReviewsWidget",
230
- icon: "lucide-star",
231
- title: "PRODUCT.WIDGETS.REVIEWS",
232
- badge: reviewsCount,
233
- isVisible: computed(() => !!product.value?.id),
234
- onClick: () => openBlade({ name: "ReviewsList" }),
235
- },
236
- ]);
237
-
238
- async function save() {
239
- await api.saveProduct(product.value);
240
- // Refresh all widget counts after saving
241
- refreshAll();
242
- }
243
- </script>
244
- ```
245
-
246
- ## Prerequisites
247
-
248
- **`useBladeWidgets`**:
249
- - Must be called inside a blade component rendered by `VcBladeSlot` (requires `BladeDescriptorKey` injection).
250
- - `WidgetService` must be provided in the component tree (automatically available in vc-shell apps).
251
- - Calling outside a blade context throws an error with a descriptive message.
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
-
257
- ## Details
258
-
259
- - **Lifecycle management**: Widgets are registered in `onMounted` and unregistered in `onUnmounted`. This ensures the WidgetService always reflects the currently visible blades.
260
- - **Blade ID resolution**: The composable injects `BladeDescriptorKey` to determine which blade the widgets belong to. Each blade has its own isolated widget list.
261
- - **Trigger pattern**: The `onRefresh` callback is stored as a `trigger` on the registered widget. When `refresh(id)` or `refreshAll()` is called, the trigger is invoked. Widgets without `onRefresh` are silently skipped.
262
-
263
- ## Tips
264
-
265
- - Use `refreshAll()` after any blade operation that might change widget badge counts (save, delete, import).
266
- - The `badge` field accepts both numbers and strings. Use a string for non-numeric badges like "New" or "!".
267
- - Keep widget IDs unique within a blade. Duplicate IDs will overwrite previous registrations.
268
- - Combine with `defineBladeContext` to expose blade entity data that widget components (non-headless) can consume via `injectBladeContext`.
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
-
299
- ## Related
300
-
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
305
- - `VcBladeSlot` -- the blade wrapper that provides `BladeDescriptorKey`
@@ -1,146 +0,0 @@
1
- # useGlobalSearch
2
-
3
- Provides access to the global search service state: per-blade search visibility, search queries, and methods to toggle/close search. The service maintains a map of blade IDs to search state, allowing each blade in the stack to have its own independent search input. The state is shared through Vue's provide/inject system and automatically cleaned up when the scope is disposed.
4
-
5
- Also exports `provideGlobalSearch()` for framework-level initialization.
6
-
7
- ## When to Use
8
-
9
- - In blade toolbars or headers to toggle and read the search input visibility for a specific blade
10
- - To programmatically set or read the search query for a specific blade (e.g., from a URL parameter or external trigger)
11
- - To integrate search with the blade toolbar's search icon button
12
- - When NOT to use: for table-level filtering within a blade, use VcDataTable's built-in search header. Global search is for blade-level search that affects the entire blade's content.
13
-
14
- ## Quick Start
15
-
16
- ```vue
17
- <script setup lang="ts">
18
- import { useGlobalSearch } from '@vc-shell/framework';
19
- import { computed, watch } from 'vue';
20
-
21
- const props = defineProps<{ bladeId: string }>();
22
- const { isSearchVisible, searchQuery, toggleSearch, setSearchQuery, closeSearch } = useGlobalSearch();
23
-
24
- // Reactive accessors scoped to this blade
25
- const isVisible = computed(() => isSearchVisible.value[props.bladeId] ?? false);
26
- const query = computed(() => searchQuery.value[props.bladeId] ?? '');
27
-
28
- // React to search query changes
29
- watch(query, (newQuery) => {
30
- if (newQuery.length >= 2) {
31
- fetchResults(newQuery);
32
- }
33
- });
34
-
35
- function onSearchToggle() {
36
- toggleSearch(props.bladeId);
37
- }
38
-
39
- function onSearchClose() {
40
- closeSearch(props.bladeId);
41
- }
42
- </script>
43
-
44
- <template>
45
- <div>
46
- <VcButton icon="fas fa-search" @click="onSearchToggle" />
47
- <VcInput
48
- v-if="isVisible"
49
- :model-value="query"
50
- placeholder="Search..."
51
- @update:model-value="(val) => setSearchQuery(bladeId, val)"
52
- @keydown.escape="onSearchClose"
53
- />
54
- </div>
55
- </template>
56
- ```
57
-
58
- ## API
59
-
60
- ### Parameters
61
-
62
- None.
63
-
64
- ### Returns (`GlobalSearchState`)
65
-
66
- | Property / Method | Type | Description |
67
- |-------------------|------|-------------|
68
- | `isSearchVisible` | `Ref<Record<string, boolean>>` | Map of blade IDs to search visibility state. Access via `isSearchVisible.value[bladeId]`. |
69
- | `searchQuery` | `Ref<Record<string, string>>` | Map of blade IDs to current search query strings. Access via `searchQuery.value[bladeId]`. |
70
- | `toggleSearch` | `(bladeId: string) => void` | Toggles search visibility for a blade. If visible, hides it (and clears the query). If hidden, shows it. |
71
- | `setSearchQuery` | `(bladeId: string, query: string) => void` | Sets the search query for a blade. Does not affect visibility. |
72
- | `closeSearch` | `(bladeId: string) => void` | Hides the search input for a blade and clears the query. |
73
-
74
- ### Additional Exports
75
-
76
- | Export | Description |
77
- |--------|-------------|
78
- | `provideGlobalSearch()` | Creates and provides the global search service. Idempotent -- returns existing service if already provided. Cleans up all state on scope disposal. |
79
-
80
- ## How It Works
81
-
82
- The service is a simple reactive state container with two `Ref<Record<string, ...>>` maps. Each blade ID is a key in these maps. This design means:
83
-
84
- 1. **Blades are isolated**: Each blade has its own search visibility and query. Opening search in one blade does not affect others in the stack.
85
- 2. **Lazy initialization**: A blade's entry in the map is created on first `toggleSearch` or `setSearchQuery` call. Reading a non-existent key returns `undefined`, which the consumer treats as `false`/empty.
86
- 3. **Cleanup**: When `provideGlobalSearch()` scope is disposed (e.g., app unmount), both maps are cleared.
87
-
88
- ## Recipe: Toolbar Search Button with Badge Indicator
89
-
90
- ```vue
91
- <script setup lang="ts">
92
- import { useGlobalSearch } from '@vc-shell/framework';
93
- import { computed } from 'vue';
94
-
95
- const props = defineProps<{ bladeId: string }>();
96
- const { isSearchVisible, searchQuery, toggleSearch } = useGlobalSearch();
97
-
98
- const hasActiveSearch = computed(() => {
99
- const query = searchQuery.value[props.bladeId];
100
- return query != null && query.length > 0;
101
- });
102
- </script>
103
-
104
- <template>
105
- <VcButton
106
- :icon="hasActiveSearch ? 'fas fa-search-plus' : 'fas fa-search'"
107
- :variant="hasActiveSearch ? 'primary' : 'ghost'"
108
- @click="toggleSearch(bladeId)"
109
- />
110
- </template>
111
- ```
112
-
113
- ## Recipe: Pre-Populating Search from URL Query Parameter
114
-
115
- ```vue
116
- <script setup lang="ts">
117
- import { useGlobalSearch } from '@vc-shell/framework';
118
- import { useRoute } from 'vue-router';
119
- import { onMounted } from 'vue';
120
-
121
- const props = defineProps<{ bladeId: string }>();
122
- const route = useRoute();
123
- const { setSearchQuery, toggleSearch } = useGlobalSearch();
124
-
125
- onMounted(() => {
126
- const urlQuery = route.query.search as string | undefined;
127
- if (urlQuery) {
128
- setSearchQuery(props.bladeId, urlQuery);
129
- toggleSearch(props.bladeId); // make the search input visible
130
- }
131
- });
132
- </script>
133
- ```
134
-
135
- ## Tips
136
-
137
- - **`toggleSearch` clears the query when hiding.** This is by design -- when the user closes the search, the query is reset. If you need to preserve the query across toggle cycles, store it separately.
138
- - **Use blade ID, not component instance ID.** The blade ID comes from the blade descriptor and is stable across re-renders. Using a component's `uid` would break if the component is recreated.
139
- - **State is not persisted.** Unlike sidebar state, search state is in-memory only. Refreshing the page clears all search queries. Use URL query parameters if you need persistence.
140
- - **Calling outside VcApp throws.** Like all provide/inject composables in the framework, `useGlobalSearch()` throws an `InjectionError` if the service has not been provided.
141
-
142
- ## Related
143
-
144
- - VcDataTable search header -- table-level filtering built into the data table (different from global search)
145
- - [useToolbar](../useToolbar/) -- toolbar service for blade action buttons (often includes a search toggle button)
146
- - `framework/core/services/global-search-service.ts` -- underlying service implementation
@@ -1,83 +0,0 @@
1
- # useMenuExpanded
2
-
3
- Manages the sidebar menu expanded/collapsed and hover-expanded state with localStorage persistence. This is the low-level composable that powers the sidebar pin/unpin behavior in the vc-shell admin UI. It tracks two independent states: the permanent "pinned" state (persisted across sessions) and the transient "hover-expanded" state (active only while the user hovers over a collapsed sidebar).
4
-
5
- ## When to Use
6
-
7
- - Low-level control over sidebar pin and hover state
8
- - Building a custom sidebar component that needs expand/collapse persistence
9
- - When NOT to use: prefer `useSidebarState` which wraps this composable and adds mobile menu support, derived `isExpanded` computed, and responsive breakpoint handling
10
-
11
- ## Basic Usage
12
-
13
- ```typescript
14
- import { useMenuExpanded } from '@vc-shell/framework';
15
-
16
- const { isExpanded, toggleExpanded, isHoverExpanded, toggleHoverExpanded } = useMenuExpanded();
17
-
18
- // Toggle pin state (persisted to localStorage)
19
- toggleExpanded();
20
-
21
- // Hover expand with 200ms delay
22
- toggleHoverExpanded(true); // opens after delay
23
- toggleHoverExpanded(false); // closes immediately
24
- ```
25
-
26
- ## API
27
-
28
- ### Returns
29
-
30
- | Property | Type | Description |
31
- |---|---|---|
32
- | `isExpanded` | `Ref<boolean>` | Pinned state, persisted via `useLocalStorage` |
33
- | `toggleExpanded` | `() => void` | Toggle the pinned state |
34
- | `isHoverExpanded` | `Ref<boolean>` | Temporary hover expansion (not persisted) |
35
- | `toggleHoverExpanded` | `(shouldExpand?: boolean) => void` | Set hover state; opening has a 200ms delay, closing is immediate |
36
-
37
- ## Recipe: Custom Sidebar with Hover Expand
38
-
39
- A common pattern is binding mouse events on the sidebar rail so the menu previews its full width on hover, without permanently pinning it open:
40
-
41
- ```vue
42
- <script setup lang="ts">
43
- import { computed } from "vue";
44
- import { useMenuExpanded } from "@vc-shell/framework";
45
-
46
- const { isExpanded, toggleExpanded, isHoverExpanded, toggleHoverExpanded } = useMenuExpanded();
47
-
48
- // The sidebar is visually expanded if pinned OR hover-expanded
49
- const isVisuallyExpanded = computed(() => isExpanded.value || isHoverExpanded.value);
50
- </script>
51
-
52
- <template>
53
- <aside
54
- :class="{ 'sidebar--expanded': isVisuallyExpanded }"
55
- @mouseenter="toggleHoverExpanded(true)"
56
- @mouseleave="toggleHoverExpanded(false)"
57
- >
58
- <nav>
59
- <!-- menu items -->
60
- </nav>
61
- <button @click="toggleExpanded">
62
- {{ isExpanded ? 'Unpin' : 'Pin' }}
63
- </button>
64
- </aside>
65
- </template>
66
- ```
67
-
68
- ## Details
69
-
70
- - **Storage key scoping**: The key is scoped per application using the first URL path segment: `VC_APP_MENU_EXPANDED_{appName}`. For example, if the app is hosted at `/vendor-portal/`, the key is `VC_APP_MENU_EXPANDED_vendor-portal`. This allows multiple vc-shell apps on the same domain to maintain independent sidebar states.
71
- - **Hover delay**: Opening uses a 200ms debounce to prevent accidental expansion when the cursor briefly passes over the sidebar. Closing is immediate to feel responsive.
72
- - **Cleanup**: Pending hover timeouts are cleaned up via `onScopeDispose` to prevent memory leaks when the composable's effect scope is destroyed.
73
- - **Default state**: The sidebar starts pinned open (`true`) on first visit, which is the most user-friendly default for new users.
74
-
75
- ## Tips
76
-
77
- - If you call `toggleHoverExpanded(true)` and then `toggleHoverExpanded(false)` within the 200ms window, the expansion is canceled -- the timeout is cleared before it fires.
78
- - The composable does not handle mobile breakpoints. For responsive behavior, use `useSidebarState` instead, which combines `useMenuExpanded` with mobile detection.
79
- - Each call to `useMenuExpanded()` creates a new instance with its own timeout tracking, but they share the same localStorage-backed `isExpanded` ref (via `useLocalStorage`). Multiple instances will stay in sync for the pinned state.
80
-
81
- ## Related
82
-
83
- - `useSidebarState` -- higher-level composable that adds mobile menu and derived `isExpanded` computed