@vc-shell/vc-app-skill 2.0.0-alpha.31 → 2.0.0-alpha.33
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 +19 -0
- package/package.json +1 -1
- package/runtime/VERSION +1 -1
- package/runtime/agents/migration-agent.md +83 -0
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/composables/bladeContext/index.docs.md +111 -0
- package/runtime/knowledge/docs/core/composables/useBladeForm/useBladeForm.docs.md +167 -0
- package/runtime/knowledge/docs/core/composables/useBladeWidgets/index.docs.md +305 -0
- package/runtime/knowledge/docs/core/composables/useMenuExpanded/index.docs.md +83 -0
- package/runtime/knowledge/docs/core/utilities/thumbnail/thumbnail.docs.md +116 -0
- package/runtime/knowledge/docs/shell/components/change-password-button/change-password-button.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/atoms/vc-card/vc-card.docs.md +4 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-accordion/vc-accordion.docs.md +4 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-checkbox/vc-checkbox.docs.md +5 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-checkbox-group/vc-checkbox-group.docs.md +5 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-color-input/vc-color-input.docs.md +5 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-date-picker/vc-date-picker.docs.md +7 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-editor/vc-editor.docs.md +5 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-field/vc-field.docs.md +5 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-file-upload/vc-file-upload.docs.md +5 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-input/vc-input.docs.md +7 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-input-currency/vc-input-currency.docs.md +7 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-input-dropdown/vc-input-dropdown.docs.md +7 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-multivalue/vc-multivalue.docs.md +7 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-radio-button/vc-radio-button.docs.md +5 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-radio-group/vc-radio-group.docs.md +5 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-rating/vc-rating.docs.md +5 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-select/vc-select.docs.md +7 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-slider/vc-slider.docs.md +5 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-switch/vc-switch.docs.md +5 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-textarea/vc-textarea.docs.md +7 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-blade/vc-blade.docs.md +30 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-data-table/vc-data-table.docs.md +28 -0
- package/runtime/knowledge/migration-prompts/blade-form-migration.md +246 -0
- package/runtime/knowledge/migration-prompts/blade-props-migration.md +195 -0
- package/runtime/knowledge/migration-prompts/notifications-migration.md +218 -0
- package/runtime/knowledge/migration-prompts/nswag-migration.md +248 -0
- package/runtime/knowledge/migration-prompts/widgets-migration.md +157 -0
- package/runtime/vc-app.md +126 -0
- package/runtime/knowledge/docs/core/constants/constants.docs.md +0 -185
- /package/runtime/knowledge/docs/shell/{pages → auth}/ChangePasswordPage/change-password-page.docs.md +0 -0
|
@@ -0,0 +1,305 @@
|
|
|
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`
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Thumbnail URL Utility
|
|
2
|
+
|
|
3
|
+
Transforms full-size image URLs into thumbnail variants by appending size suffixes before the file extension. VirtoCommerce backend generates thumbnails automatically with these suffixes.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
- Use in any component that displays images at a known size smaller than the original
|
|
8
|
+
- Reduces bandwidth and improves page load time significantly
|
|
9
|
+
- Especially important for image lists (tables, galleries, cards)
|
|
10
|
+
|
|
11
|
+
## Available Sizes
|
|
12
|
+
|
|
13
|
+
### Named Presets
|
|
14
|
+
|
|
15
|
+
| Preset | Use Case |
|
|
16
|
+
|--------|----------|
|
|
17
|
+
| `sm` | Small icons, table cells, avatar thumbnails |
|
|
18
|
+
| `md` | Medium previews, cards |
|
|
19
|
+
| `lg` | Large previews, hero images |
|
|
20
|
+
|
|
21
|
+
### Pixel Sizes
|
|
22
|
+
|
|
23
|
+
| Size | Pixels | Use Case |
|
|
24
|
+
|------|--------|----------|
|
|
25
|
+
| `64x64` | 64px | Table cells, tiny thumbnails |
|
|
26
|
+
| `128x128` | 128px | Small tiles, list items |
|
|
27
|
+
| `168x168` | 168px | Medium tiles |
|
|
28
|
+
| `216x216` | 216px | Gallery tiles (md) |
|
|
29
|
+
| `348x348` | 348px | Large gallery tiles |
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
|
|
33
|
+
### `getThumbnailUrl(url, size?)`
|
|
34
|
+
|
|
35
|
+
Transforms an image URL by inserting a size suffix before the file extension.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { getThumbnailUrl } from "@core/utilities/thumbnail";
|
|
39
|
+
|
|
40
|
+
getThumbnailUrl("https://cdn.example.com/photo.jpg", "sm")
|
|
41
|
+
// → "https://cdn.example.com/photo_sm.jpg"
|
|
42
|
+
|
|
43
|
+
getThumbnailUrl("https://cdn.example.com/photo.jpg", "128x128")
|
|
44
|
+
// → "https://cdn.example.com/photo_128x128.jpg"
|
|
45
|
+
|
|
46
|
+
getThumbnailUrl("https://cdn.example.com/photo.jpg")
|
|
47
|
+
// → "https://cdn.example.com/photo.jpg" (unchanged)
|
|
48
|
+
|
|
49
|
+
getThumbnailUrl(undefined, "sm")
|
|
50
|
+
// → undefined
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Parameters:**
|
|
54
|
+
- `url` — Original image URL (string or undefined)
|
|
55
|
+
- `size` — Thumbnail size preset or pixel dimensions (optional)
|
|
56
|
+
|
|
57
|
+
**Returns:** Transformed URL, or original URL if size not specified
|
|
58
|
+
|
|
59
|
+
### `getBestThumbnailSize(displaySize)`
|
|
60
|
+
|
|
61
|
+
Maps a CSS pixel display size to the best-fit thumbnail preset. Picks the smallest thumbnail that is >= the display size.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { getBestThumbnailSize } from "@core/utilities/thumbnail";
|
|
65
|
+
|
|
66
|
+
getBestThumbnailSize(48) // → "64x64"
|
|
67
|
+
getBestThumbnailSize(96) // → "128x128"
|
|
68
|
+
getBestThumbnailSize(200) // → "216x216"
|
|
69
|
+
getBestThumbnailSize(500) // → "lg"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage in Components
|
|
73
|
+
|
|
74
|
+
### VcImage
|
|
75
|
+
|
|
76
|
+
```vue
|
|
77
|
+
<VcImage
|
|
78
|
+
:src="product.imgSrc"
|
|
79
|
+
thumbnail-size="sm"
|
|
80
|
+
size="s"
|
|
81
|
+
/>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### VcGallery
|
|
85
|
+
|
|
86
|
+
Gallery auto-maps `size` prop to thumbnail size. Override with `thumbnailSize`:
|
|
87
|
+
|
|
88
|
+
```vue
|
|
89
|
+
<!-- Auto: sm→128x128, md→216x216, lg→348x348 -->
|
|
90
|
+
<VcGallery :images="images" size="md" />
|
|
91
|
+
|
|
92
|
+
<!-- Explicit override -->
|
|
93
|
+
<VcGallery :images="images" thumbnail-size="128x128" />
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Gallery preview uses `64x64` for the thumbnail strip and full-size for the main image automatically.
|
|
97
|
+
|
|
98
|
+
### VcDataTable (CellImage)
|
|
99
|
+
|
|
100
|
+
Table image cells use `sm` thumbnail by default — no action needed.
|
|
101
|
+
|
|
102
|
+
## Types
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
type ThumbnailPreset = "sm" | "md" | "lg";
|
|
106
|
+
type ThumbnailPixelSize = "64x64" | "128x128" | "168x168" | "216x216" | "348x348";
|
|
107
|
+
type ThumbnailSize = ThumbnailPreset | ThumbnailPixelSize;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Notes
|
|
111
|
+
|
|
112
|
+
- The utility only transforms the URL — the backend must have generated the thumbnail for the URL to resolve
|
|
113
|
+
- If a thumbnail doesn't exist on the server, the browser will show a broken image. Ensure your asset pipeline generates all required sizes.
|
|
114
|
+
- The suffix is inserted before the file extension: `photo.jpg` → `photo_sm.jpg`
|
|
115
|
+
- URLs without a file extension are returned unchanged
|
|
116
|
+
- Already-suffixed URLs are not double-transformed
|
|
@@ -91,4 +91,4 @@ export default {
|
|
|
91
91
|
- [SettingsMenu](../settings-menu/settings-menu.docs.md) -- parent container
|
|
92
92
|
- [SettingsMenuItem](../settings-menu-item/settings-menu-item.docs.md) -- base menu item used internally
|
|
93
93
|
- [LogoutButton](../logout-button/logout-button.docs.md) -- sibling account action
|
|
94
|
-
- [ChangePasswordPage](../../
|
|
94
|
+
- [ChangePasswordPage](../../auth/ChangePasswordPage/change-password-page.docs.md) -- full-page variant for expired passwords
|
|
@@ -351,3 +351,7 @@ const validationHeader = computed(() =>
|
|
|
351
351
|
- [VcContainer](../vc-container/) -- scrollable content wrapper without header or collapsing
|
|
352
352
|
- [VcBanner](../vc-banner/) -- for alert/notification messages rather than content grouping
|
|
353
353
|
- [VcCol](../vc-col/) / [VcRow](../vc-row/) -- for grid-based layout within card bodies
|
|
354
|
+
|
|
355
|
+
## Skeleton / Loading State
|
|
356
|
+
|
|
357
|
+
When placed inside a `VcBlade` with `loading=true`, VcCard shows a skeleton header (if the `header` prop is set) while its body content renders normally — child components self-skeletonize via their own `BladeLoadingKey` injection.
|
|
@@ -250,3 +250,7 @@ interface AccordionItem {
|
|
|
250
250
|
## Related Components
|
|
251
251
|
|
|
252
252
|
- [VcAccordionItem](./_internal/vc-accordion-item/) -- individual accordion panel (used internally and available via the default slot)
|
|
253
|
+
|
|
254
|
+
## Skeleton / Loading State
|
|
255
|
+
|
|
256
|
+
When placed inside a `VcBlade` with `loading=true`, the component automatically renders a skeleton placeholder matching its visual footprint. No additional props or configuration needed.
|
|
@@ -323,3 +323,8 @@ const selected = ref<string[]>([]);
|
|
|
323
323
|
- [VcSwitch](../vc-switch/) -- toggle switch for on/off settings
|
|
324
324
|
- [VcRadioButton](../vc-radio-button/) -- mutually exclusive single selection
|
|
325
325
|
- [VcInputGroup](../vc-input-group/) -- semantic wrapper for grouping checkboxes or radio buttons
|
|
326
|
+
|
|
327
|
+
## Skeleton / Loading State
|
|
328
|
+
|
|
329
|
+
When placed inside a `VcBlade` with `loading=true`, the component renders a skeleton placeholder matching its shape — a control indicator and label block. No configuration needed.
|
|
330
|
+
|
package/runtime/knowledge/docs/ui/components/molecules/vc-checkbox-group/vc-checkbox-group.docs.md
CHANGED
|
@@ -144,3 +144,8 @@ interface CheckboxGroupOption<V = string | number | boolean> {
|
|
|
144
144
|
- [VcCheckbox](../vc-checkbox/) — individual checkbox component
|
|
145
145
|
- [VcRadioGroup](../vc-radio-group/) — mutually exclusive option group
|
|
146
146
|
- [VcInputGroup](../vc-input-group/) — generic form field group (used internally)
|
|
147
|
+
|
|
148
|
+
## Skeleton / Loading State
|
|
149
|
+
|
|
150
|
+
When placed inside a `VcBlade` with `loading=true`, the component renders a skeleton placeholder matching its shape — a control indicator and label block. No configuration needed.
|
|
151
|
+
|
package/runtime/knowledge/docs/ui/components/molecules/vc-color-input/vc-color-input.docs.md
CHANGED
|
@@ -151,3 +151,8 @@ The native color picker does not support alpha/transparency. If you need RGBA co
|
|
|
151
151
|
|
|
152
152
|
- [VcInput](../vc-input/) -- general-purpose input (delegates to VcColorInput for `type="color"`)
|
|
153
153
|
- [VcField](../vc-field/) -- read-only field display (for showing a color value without editing)
|
|
154
|
+
|
|
155
|
+
## Skeleton / Loading State
|
|
156
|
+
|
|
157
|
+
When placed inside a `VcBlade` with `loading=true`, the component automatically renders a skeleton placeholder matching its visual footprint. No additional props or configuration needed.
|
|
158
|
+
|
package/runtime/knowledge/docs/ui/components/molecules/vc-date-picker/vc-date-picker.docs.md
CHANGED
|
@@ -359,3 +359,10 @@ Uses the same `--input-*` variables as VcInput for consistent styling across all
|
|
|
359
359
|
|
|
360
360
|
- [VcInput](../vc-input/) -- general-purpose input; delegates to VcDatePicker for `type="date"` and `type="datetime-local"`
|
|
361
361
|
- [VcMultivalue](../vc-multivalue/) -- can handle multiple date values with `type="date"`
|
|
362
|
+
|
|
363
|
+
## Skeleton / Loading State
|
|
364
|
+
|
|
365
|
+
When placed inside a `VcBlade` with `loading=true`, the component automatically renders a skeleton placeholder matching its visual footprint — a label block (when the `label` prop is set) and an input-shaped block. No additional props or configuration needed.
|
|
366
|
+
|
|
367
|
+
This behavior is powered by `BladeLoadingKey` via Vue's provide/inject. The component injects the loading state from the nearest `VcBlade` ancestor.
|
|
368
|
+
|
|
@@ -298,3 +298,8 @@ const content = ref("<h1>Title</h1>");
|
|
|
298
298
|
|
|
299
299
|
- [VcTextarea](../vc-textarea/) -- plain multi-line text input (no formatting)
|
|
300
300
|
- [VcInput](../vc-input/) -- single-line text input
|
|
301
|
+
|
|
302
|
+
## Skeleton / Loading State
|
|
303
|
+
|
|
304
|
+
When placed inside a `VcBlade` with `loading=true`, the component automatically renders a skeleton placeholder matching its visual footprint. No additional props or configuration needed.
|
|
305
|
+
|
|
@@ -166,3 +166,8 @@ The `type` prop affects rendering, not validation. Setting `type="email"` does n
|
|
|
166
166
|
- [VcInput](../vc-input/) -- editable text field (use instead when user input is needed)
|
|
167
167
|
- [VcLabel](../../atoms/vc-label/) -- standalone label atom used internally
|
|
168
168
|
- [VcCol](../../atoms/vc-col/) -- column layout atom used for aspect ratio
|
|
169
|
+
|
|
170
|
+
## Skeleton / Loading State
|
|
171
|
+
|
|
172
|
+
When placed inside a `VcBlade` with `loading=true`, the component automatically renders a skeleton placeholder matching its visual footprint. No additional props or configuration needed.
|
|
173
|
+
|
package/runtime/knowledge/docs/ui/components/molecules/vc-file-upload/vc-file-upload.docs.md
CHANGED
|
@@ -322,3 +322,8 @@ async function onUpload(files: FileList) {
|
|
|
322
322
|
- [VcGallery](../../organisms/vc-gallery/) -- Full image gallery with preview, reorder, drag-and-drop sorting, and upload
|
|
323
323
|
- [VcImageTile](../vc-image-tile/) -- Image display tile used inside galleries
|
|
324
324
|
- [VcHint](../../atoms/vc-hint/) -- Used internally for error message display
|
|
325
|
+
|
|
326
|
+
## Skeleton / Loading State
|
|
327
|
+
|
|
328
|
+
When placed inside a `VcBlade` with `loading=true`, the component automatically renders a skeleton placeholder matching its visual footprint. No additional props or configuration needed.
|
|
329
|
+
|
|
@@ -786,3 +786,10 @@ VcInput follows WAI-ARIA best practices for form fields:
|
|
|
786
786
|
- [VcInputGroup](../vc-input-group/) -- Groups multiple inputs with shared label, error state, and disabled state
|
|
787
787
|
- [VcLabel](../../atoms/vc-label/) -- The label atom used internally by VcInput
|
|
788
788
|
- [VcHint](../../atoms/vc-hint/) -- The hint/error atom used internally by VcInput
|
|
789
|
+
|
|
790
|
+
## Skeleton / Loading State
|
|
791
|
+
|
|
792
|
+
When placed inside a `VcBlade` with `loading=true`, the component automatically renders a skeleton placeholder matching its visual footprint — a label block (when the `label` prop is set) and an input-shaped block. No additional props or configuration needed.
|
|
793
|
+
|
|
794
|
+
This behavior is powered by `BladeLoadingKey` via Vue's provide/inject. The component injects the loading state from the nearest `VcBlade` ancestor.
|
|
795
|
+
|
package/runtime/knowledge/docs/ui/components/molecules/vc-input-currency/vc-input-currency.docs.md
CHANGED
|
@@ -368,3 +368,10 @@ Additionally inherits all `--input-*` CSS variables from VcInput/VcInputDropdown
|
|
|
368
368
|
- [VcInputDropdown](../vc-input-dropdown/) -- the underlying composite input + dropdown component
|
|
369
369
|
- [VcInput](../vc-input/) -- plain input for non-currency numbers
|
|
370
370
|
- [VcSelect](../vc-select/) -- standalone dropdown selection
|
|
371
|
+
|
|
372
|
+
## Skeleton / Loading State
|
|
373
|
+
|
|
374
|
+
When placed inside a `VcBlade` with `loading=true`, the component automatically renders a skeleton placeholder matching its visual footprint — a label block (when the `label` prop is set) and an input-shaped block. No additional props or configuration needed.
|
|
375
|
+
|
|
376
|
+
This behavior is powered by `BladeLoadingKey` via Vue's provide/inject. The component injects the loading state from the nearest `VcBlade` ancestor.
|
|
377
|
+
|
package/runtime/knowledge/docs/ui/components/molecules/vc-input-dropdown/vc-input-dropdown.docs.md
CHANGED
|
@@ -305,3 +305,10 @@ Replace the default dropdown toggle with a custom element using the `button` slo
|
|
|
305
305
|
- [VcInputCurrency](../vc-input-currency/) -- Currency-specific variant with built-in locale formatting
|
|
306
306
|
- [VcInput](../vc-input/) -- Standalone text input (used internally)
|
|
307
307
|
- [VcSelect](../vc-select/) -- Standalone dropdown selection (used internally)
|
|
308
|
+
|
|
309
|
+
## Skeleton / Loading State
|
|
310
|
+
|
|
311
|
+
When placed inside a `VcBlade` with `loading=true`, the component automatically renders a skeleton placeholder matching its visual footprint — a label block (when the `label` prop is set) and an input-shaped block. No additional props or configuration needed.
|
|
312
|
+
|
|
313
|
+
This behavior is powered by `BladeLoadingKey` via Vue's provide/inject. The component injects the loading state from the nearest `VcBlade` ancestor.
|
|
314
|
+
|
|
@@ -345,3 +345,10 @@ The component uses `--multivalue-*` variables that fall back to `--select-*` tok
|
|
|
345
345
|
- [VcSelect](../vc-select/) -- dropdown for single/multi selection without manual entry
|
|
346
346
|
- [VcInput](../vc-input/) -- single-value text input
|
|
347
347
|
- [VcInputGroup](../vc-input-group/) -- semantic wrapper for grouping form controls
|
|
348
|
+
|
|
349
|
+
## Skeleton / Loading State
|
|
350
|
+
|
|
351
|
+
When placed inside a `VcBlade` with `loading=true`, the component automatically renders a skeleton placeholder matching its visual footprint — a label block (when the `label` prop is set) and an input-shaped block. No additional props or configuration needed.
|
|
352
|
+
|
|
353
|
+
This behavior is powered by `BladeLoadingKey` via Vue's provide/inject. The component injects the loading state from the nearest `VcBlade` ancestor.
|
|
354
|
+
|
package/runtime/knowledge/docs/ui/components/molecules/vc-radio-button/vc-radio-button.docs.md
CHANGED
|
@@ -161,3 +161,8 @@ Do not mix `binary` mode with regular `value` comparison in the same group. Bina
|
|
|
161
161
|
- [VcSwitch](../vc-switch/) -- for on/off toggles
|
|
162
162
|
- [VcInputGroup](../vc-input-group/) -- semantic wrapper with `role="radiogroup"`
|
|
163
163
|
- [VcSelect](../vc-select/) -- dropdown for larger option sets
|
|
164
|
+
|
|
165
|
+
## Skeleton / Loading State
|
|
166
|
+
|
|
167
|
+
When placed inside a `VcBlade` with `loading=true`, the component renders a skeleton placeholder matching its shape — a control indicator and label block. No configuration needed.
|
|
168
|
+
|