@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +1 -1
  3. package/runtime/VERSION +1 -1
  4. package/runtime/agents/migration-agent.md +83 -0
  5. package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
  6. package/runtime/knowledge/docs/core/composables/bladeContext/index.docs.md +111 -0
  7. package/runtime/knowledge/docs/core/composables/useBladeForm/useBladeForm.docs.md +167 -0
  8. package/runtime/knowledge/docs/core/composables/useBladeWidgets/index.docs.md +305 -0
  9. package/runtime/knowledge/docs/core/composables/useMenuExpanded/index.docs.md +83 -0
  10. package/runtime/knowledge/docs/core/utilities/thumbnail/thumbnail.docs.md +116 -0
  11. package/runtime/knowledge/docs/shell/components/change-password-button/change-password-button.docs.md +1 -1
  12. package/runtime/knowledge/docs/ui/components/atoms/vc-card/vc-card.docs.md +4 -0
  13. package/runtime/knowledge/docs/ui/components/molecules/vc-accordion/vc-accordion.docs.md +4 -0
  14. package/runtime/knowledge/docs/ui/components/molecules/vc-checkbox/vc-checkbox.docs.md +5 -0
  15. package/runtime/knowledge/docs/ui/components/molecules/vc-checkbox-group/vc-checkbox-group.docs.md +5 -0
  16. package/runtime/knowledge/docs/ui/components/molecules/vc-color-input/vc-color-input.docs.md +5 -0
  17. package/runtime/knowledge/docs/ui/components/molecules/vc-date-picker/vc-date-picker.docs.md +7 -0
  18. package/runtime/knowledge/docs/ui/components/molecules/vc-editor/vc-editor.docs.md +5 -0
  19. package/runtime/knowledge/docs/ui/components/molecules/vc-field/vc-field.docs.md +5 -0
  20. package/runtime/knowledge/docs/ui/components/molecules/vc-file-upload/vc-file-upload.docs.md +5 -0
  21. package/runtime/knowledge/docs/ui/components/molecules/vc-input/vc-input.docs.md +7 -0
  22. package/runtime/knowledge/docs/ui/components/molecules/vc-input-currency/vc-input-currency.docs.md +7 -0
  23. package/runtime/knowledge/docs/ui/components/molecules/vc-input-dropdown/vc-input-dropdown.docs.md +7 -0
  24. package/runtime/knowledge/docs/ui/components/molecules/vc-multivalue/vc-multivalue.docs.md +7 -0
  25. package/runtime/knowledge/docs/ui/components/molecules/vc-radio-button/vc-radio-button.docs.md +5 -0
  26. package/runtime/knowledge/docs/ui/components/molecules/vc-radio-group/vc-radio-group.docs.md +5 -0
  27. package/runtime/knowledge/docs/ui/components/molecules/vc-rating/vc-rating.docs.md +5 -0
  28. package/runtime/knowledge/docs/ui/components/molecules/vc-select/vc-select.docs.md +7 -0
  29. package/runtime/knowledge/docs/ui/components/molecules/vc-slider/vc-slider.docs.md +5 -0
  30. package/runtime/knowledge/docs/ui/components/molecules/vc-switch/vc-switch.docs.md +5 -0
  31. package/runtime/knowledge/docs/ui/components/molecules/vc-textarea/vc-textarea.docs.md +7 -0
  32. package/runtime/knowledge/docs/ui/components/organisms/vc-blade/vc-blade.docs.md +30 -0
  33. package/runtime/knowledge/docs/ui/components/organisms/vc-data-table/vc-data-table.docs.md +28 -0
  34. package/runtime/knowledge/migration-prompts/blade-form-migration.md +246 -0
  35. package/runtime/knowledge/migration-prompts/blade-props-migration.md +195 -0
  36. package/runtime/knowledge/migration-prompts/notifications-migration.md +218 -0
  37. package/runtime/knowledge/migration-prompts/nswag-migration.md +248 -0
  38. package/runtime/knowledge/migration-prompts/widgets-migration.md +157 -0
  39. package/runtime/vc-app.md +126 -0
  40. package/runtime/knowledge/docs/core/constants/constants.docs.md +0 -185
  41. /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](../../pages/ChangePasswordPage/change-password-page.docs.md) -- full-page variant for expired passwords
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
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
+