@vc-shell/vc-app-skill 2.0.0-alpha.30 → 2.0.0-alpha.32

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 (18) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +1 -1
  3. package/runtime/VERSION +1 -1
  4. package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
  5. package/runtime/knowledge/docs/core/composables/useBladeForm/useBladeForm.docs.md +113 -0
  6. package/runtime/knowledge/docs/core/composables/useDynamicProperties/useDynamicProperties.docs.md +104 -105
  7. package/runtime/knowledge/docs/core/composables/useSlowNetworkDetection/useSlowNetworkDetection.docs.md +4 -4
  8. package/runtime/knowledge/docs/core/utilities/thumbnail/thumbnail.docs.md +116 -0
  9. package/runtime/knowledge/docs/shell/components/change-password-button/change-password-button.docs.md +1 -1
  10. package/runtime/knowledge/docs/ui/components/atoms/vc-image/vc-image.docs.md +1 -0
  11. package/runtime/knowledge/docs/ui/components/molecules/vc-image-tile/vc-image-tile.docs.md +1 -0
  12. package/runtime/knowledge/docs/ui/components/organisms/vc-blade/vc-blade.docs.md +27 -2
  13. package/runtime/knowledge/docs/ui/components/organisms/vc-gallery/vc-gallery.docs.md +55 -16
  14. package/runtime/knowledge/docs/core/composables/useGlobalSearch/useGlobalSearch.docs.md +0 -146
  15. package/runtime/knowledge/docs/shell/pages/ChangePasswordPage/change-password-page.docs.md +0 -102
  16. /package/runtime/knowledge/docs/core/composables/{useBladeContext.docs.md → bladeContext/index.docs.md} +0 -0
  17. /package/runtime/knowledge/docs/core/composables/{useBladeWidgets.docs.md → useBladeWidgets/index.docs.md} +0 -0
  18. /package/runtime/knowledge/docs/core/composables/{useMenuExpanded.docs.md → useMenuExpanded/index.docs.md} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # [2.0.0-alpha.32](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.31...v2.0.0-alpha.32) (2026-04-02)
2
+
3
+ **Note:** Version bump only for package @vc-shell/vc-app-skill
4
+
5
+ # [2.0.0-alpha.31](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.30...v2.0.0-alpha.31) (2026-04-01)
6
+
7
+ **Note:** Version bump only for package @vc-shell/vc-app-skill
8
+
1
9
  # [2.0.0-alpha.30](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.29...v2.0.0-alpha.30) (2026-03-30)
2
10
 
3
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vc-shell/vc-app-skill",
3
- "version": "2.0.0-alpha.30",
3
+ "version": "2.0.0-alpha.32",
4
4
  "description": "AI coding skill for scaffolding and generating VirtoCommerce Shell applications. Works with Claude Code, OpenCode, Gemini, Codex, Cursor.",
5
5
  "bin": "./bin/install.cjs",
6
6
  "files": [
package/runtime/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.0-alpha.30
1
+ 2.0.0-alpha.32
@@ -1 +1 @@
1
- Synced from framework at commit 37522732b on 2026-03-30T14:31:59.196Z
1
+ Synced from framework at commit fc29b1e1b on 2026-04-02T05:22:47.427Z
@@ -0,0 +1,113 @@
1
+ # useBladeForm
2
+
3
+ Unified form state management for blades. Replaces manual combination of `useForm` + `useModificationTracker` + `useBeforeUnload` + `onBeforeClose` with a single composable.
4
+
5
+ ## Import
6
+
7
+ ```ts
8
+ import { useBladeForm } from "@vc-shell/framework";
9
+ ```
10
+
11
+ ## Basic Usage
12
+
13
+ ```ts
14
+ const { item, loadItem, saveItem } = useItemData();
15
+
16
+ const form = useBladeForm({ data: item });
17
+
18
+ onMounted(async () => {
19
+ await loadItem({ id: param.value });
20
+ form.setBaseline(); // snapshot current data as pristine
21
+ });
22
+
23
+ // Toolbar
24
+ const toolbar = ref<IBladeToolbar[]>([
25
+ {
26
+ id: "save",
27
+ title: "Save",
28
+ icon: "lucide-save",
29
+ disabled: computed(() => !form.canSave.value),
30
+ async clickHandler() {
31
+ await saveItem(item.value);
32
+ form.setBaseline(); // snapshot after save
33
+ callParent("reload");
34
+ },
35
+ },
36
+ ]);
37
+ ```
38
+
39
+ ## API
40
+
41
+ ### Options
42
+
43
+ | Option | Type | Default | Description |
44
+ |--------|------|---------|-------------|
45
+ | `data` | `Ref<T>` | required | Reactive data object for the form |
46
+ | `canSaveOverride` | `ComputedRef<boolean>` | — | Additional condition for canSave |
47
+ | `autoBeforeClose` | `boolean \| ComputedRef<boolean>` | `true` | Close guard behavior |
48
+ | `autoBeforeUnload` | `boolean` | `true` | Browser tab close guard |
49
+ | `closeConfirmMessage` | `MaybeRefOrGetter<string>` | — | Custom unsaved changes message |
50
+ | `onRevert` | `() => void \| Promise<void>` | — | Custom revert handler |
51
+
52
+ ### Returns
53
+
54
+ | Property | Type | Description |
55
+ |----------|------|-------------|
56
+ | `setBaseline()` | `() => void` | Snapshot current data as pristine. Call after load and after save |
57
+ | `revert()` | `() => void \| Promise<void>` | Revert data to pristine (or call onRevert) |
58
+ | `canSave` | `ComputedRef<boolean>` | `isReady && valid && modified && canSaveOverride` |
59
+ | `isModified` | `ComputedRef<boolean>` | Data differs from pristine (false until setBaseline) |
60
+ | `isReady` | `ComputedRef<boolean>` | setBaseline() called at least once |
61
+ | `formMeta` | vee-validate meta | Form validation state |
62
+ | `setFieldError` | function | Set field error programmatically |
63
+ | `errorBag` | Ref | All field errors |
64
+
65
+ ## Lifecycle
66
+
67
+ ```
68
+ Mount → Load data → setBaseline() → User edits → Save → setBaseline()
69
+ └→ Cancel → revert()
70
+ ```
71
+
72
+ ## VcBlade Integration
73
+
74
+ `useBladeForm` auto-provides form state to `VcBlade` via inject. No need to pass `:modified` prop:
75
+
76
+ ```vue
77
+ <!-- Before -->
78
+ <VcBlade :modified="isModified" :toolbar-items="toolbar">
79
+
80
+ <!-- After -->
81
+ <VcBlade :toolbar-items="toolbar">
82
+ ```
83
+
84
+ ## Advanced: Readonly Blade
85
+
86
+ ```ts
87
+ const disabled = computed(() => !!param.value && !item.value?.canBeModified);
88
+
89
+ const form = useBladeForm({
90
+ data: item,
91
+ canSaveOverride: computed(() => !disabled.value),
92
+ autoBeforeClose: computed(() => !disabled.value), // no prompt when readonly
93
+ });
94
+ ```
95
+
96
+ ## Advanced: Custom Revert
97
+
98
+ ```ts
99
+ const form = useBladeForm({
100
+ data: item,
101
+ onRevert: () => loadItem({ id: param.value }), // reload from server
102
+ });
103
+ ```
104
+
105
+ ## Constraints
106
+
107
+ - **Must be called from component `setup()`** (or `<script setup>`). Do NOT call from shared data-composables.
108
+ - Validation rules stay in template (`<Field rules="...">`).
109
+ - `setBaseline()` must be called after data is loaded — before that, `canSave` and `isModified` are `false`.
110
+
111
+ ## Migration
112
+
113
+ See `MIGRATION_GUIDE.md` for step-by-step instructions on migrating existing modules.
@@ -1,163 +1,162 @@
1
1
  # useDynamicProperties
2
2
 
3
- Generic composable for managing dynamic (runtime-defined) property values with support for multilanguage, multivalue, dictionary, color, and measurement property types. This is one of the most complex composables in the framework because it must handle the combinatorial explosion of property configurations: a property can be multilanguage AND multivalue AND dictionary-backed, each combination requiring different get/set logic. The composable handles loading dictionary items from the API, getting/setting property values by locale, unit-of-measure lookups, and color code management.
3
+ Composable for managing dynamic (runtime-defined) property values with support for multilanguage, multivalue, dictionary, color, and measurement property types.
4
4
 
5
- The composable is generic over the property, value, dictionary item, and measurement types, allowing it to work with both the standard platform types and custom extensions.
5
+ Internally uses a strategy pattern each property type (regular, boolean, dictionary, measure, color) has a dedicated handler with `get`/`set` methods.
6
6
 
7
7
  ## When to Use
8
8
 
9
9
  - In product, category, or any entity detail blades that display platform dynamic properties
10
10
  - When properties are defined at runtime (not compile-time) and may be multilanguage or dictionary-based
11
11
  - When you need to render and edit property values that can be text, boolean, number, datetime, dictionary selection, color picker, or measurement with units
12
- - When NOT to use: for static, compile-time form fields, use standard Vue reactive state. For simple key-value pairs without multilanguage/dictionary support, plain refs are sufficient.
12
+ - When NOT to use: for static, compile-time form fields, use standard Vue reactive state
13
13
 
14
14
  ## Quick Start
15
15
 
16
16
  ```typescript
17
- import { useDynamicProperties } from '@vc-shell/framework';
18
- import { PropertyValue, PropertyDictionaryItem } from '@core/api/platform';
19
-
20
- const { loading, loadDictionaries, getPropertyValue, setPropertyValue, loadMeasurements } =
21
- useDynamicProperties(
22
- (criteria) => api.searchPropertyDictionaryItems(criteria),
23
- PropertyValue,
24
- PropertyDictionaryItem,
25
- (measureId, locale) => api.searchMeasurements(measureId, locale),
26
- );
17
+ import { useDynamicProperties } from "@vc-shell/framework";
18
+
19
+ const { getPropertyValue, setPropertyValue, loadDictionaries, loadMeasurements, loading } =
20
+ useDynamicProperties({
21
+ searchDictionary: (criteria) => api.searchPropertyDictionaryItems(criteria),
22
+ searchMeasurements: (measureId, locale) => api.searchMeasurements(measureId, locale),
23
+ });
27
24
  ```
28
25
 
29
26
  ## API
30
27
 
31
- ### Parameters
28
+ ### Options
32
29
 
33
- | Parameter | Type | Required | Description |
34
- |-----------|------|----------|-------------|
35
- | `searchDictionaryItemsFunction` | `(criteria) => Promise<TPropertyDictionaryItem[] \| undefined>` | Yes | API function to search dictionary items by property ID and keyword. Called when the user opens a dictionary dropdown. |
36
- | `PropertyValueConstructor` | `new (data?) => TPropertyValue` | Yes | Constructor class for creating property value instances. Used when creating new values during `setPropertyValue`. |
37
- | `PropertyDictionaryItemConstructor` | `new (data?) => TPropertyDictionaryItem` | Yes | Constructor class for creating dictionary item instances. Used when localizing dictionary items. |
38
- | `searchMeasurementFunction` | `(measureId, locale?) => Promise<TMeasurement[] \| undefined>` | No | API function for loading measurement/unit-of-measure dictionaries. Only needed if you have measure-type properties. |
30
+ | Option | Type | Required | Description |
31
+ |--------|------|----------|-------------|
32
+ | `searchDictionary` | `(criteria: IBasePropertyDictionaryItemSearchCriteria) => Promise<IBasePropertyDictionaryItem[] \| undefined>` | Yes | API function to search dictionary items by property ID and keyword |
33
+ | `searchMeasurements` | `(measureId: string, locale?: string) => Promise<IBaseMeasurementDictionaryItem[] \| undefined>` | No | API function for loading measurement units. Only needed for measure-type properties |
39
34
 
40
35
  ### Returns
41
36
 
42
37
  | Property | Type | Description |
43
38
  |----------|------|-------------|
44
- | `loading` | `ComputedRef<boolean>` | Whether a dictionary or measurement lookup is in progress. |
45
- | `loadDictionaries` | `(propertyId, keyword?, locale?) => Promise<TPropertyDictionaryItem[] \| undefined>` | Loads dictionary items for a property. If `locale` is provided, resolves localized values from `localizedValues`. |
46
- | `getPropertyValue` | `(property, locale) => string \| TPropertyValue[] \| boolean` | Reads the display value for a property in the given locale. Returns string for simple values, array for multivalue, boolean for boolean type. |
47
- | `setPropertyValue` | `(params: SetPropertyValueParams) => void` | Writes a value to a property. Dispatches to specialized handlers based on property type (measure, color, dictionary, regular). |
48
- | `loadMeasurements` | `(measureId, keyword?, locale?) => Promise<TMeasurement[] \| undefined>` | Loads measurement units for a measure-type property. No-op if `searchMeasurementFunction` was not provided. |
39
+ | `getPropertyValue` | `(property, locale) => PropertyDisplayValue` | Read display value for a property. Returns string, boolean, or array depending on type. Does NOT mutate the property. |
40
+ | `setPropertyValue` | `(params: SetPropertyValueParams) => void` | Write value to a property. Handles type-specific transformation and empty cleanup. |
41
+ | `loadDictionaries` | `(propertyId, keyword?, locale?) => Promise<...>` | Load dictionary items. If locale is provided, resolves localized values. |
42
+ | `loadMeasurements` | `(measureId, keyword?, locale?) => Promise<...>` | Load measurement units. No-op if `searchMeasurements` was not provided. |
43
+ | `loading` | `ComputedRef<boolean>` | Whether a dictionary lookup is in progress. |
49
44
 
50
45
  ### SetPropertyValueParams
51
46
 
52
47
  | Field | Type | Description |
53
48
  |-------|------|-------------|
54
- | `property` | `TProperty` | The property object to update. Modified in place. |
55
- | `value` | `string \| TPropertyValue[] \| TPropertyDictionaryItem[]` | The new value. Type depends on property configuration. |
56
- | `dictionary` | `TPropertyDictionaryItem[]?` | Dictionary items for dictionary properties. Required when setting a dictionary value. |
49
+ | `property` | `IBaseProperty` | The property object to update. Modified in place. |
50
+ | `value` | `string \| IBasePropertyValue[] \| (IBasePropertyDictionaryItem & { value: string })[]` | The new value. Type depends on property configuration. |
51
+ | `dictionary` | `IBasePropertyDictionaryItem[]?` | Dictionary items. Required when setting a dictionary value. |
57
52
  | `locale` | `string?` | Current locale for multilanguage properties. |
58
- | `initialProp` | `TProperty?` | Original property state. Used for empty-value detection -- if the original was empty and the new value is empty, a placeholder is preserved; otherwise, the value array is cleared. |
59
53
  | `unitOfMeasureId` | `string?` | Unit of measure ID for measure-type properties. |
60
54
  | `colorCode` | `string?` | Color hex code for color-type properties. |
61
55
 
56
+ ### PropertyDisplayValue
57
+
58
+ ```ts
59
+ type PropertyDisplayValue = string | IBasePropertyValue[] | boolean;
60
+ ```
61
+
62
62
  ## How It Works
63
63
 
64
- ### Value Getting
64
+ ### Strategy Resolution
65
65
 
66
- `getPropertyValue` dispatches based on property flags:
66
+ The composable resolves a strategy handler based on property flags:
67
67
 
68
- - **Multilanguage**: Finds the value entry matching the locale. For multivalue, returns all entries for that locale as an array. For dictionary, returns the `valueId`. If no entry exists for the locale, creates a default entry using the first available alias.
69
- - **Single-language**: Returns the first value entry. For multivalue, returns all entries as an array. For dictionary, returns the `valueId`.
70
- - **Boolean**: Returns `false` if no value entry exists (instead of empty string).
68
+ | Priority | Condition | Strategy | Handles |
69
+ |----------|-----------|----------|---------|
70
+ | 1 | `valueType === "Measure"` | measureStrategy | Numeric input with unit dropdown |
71
+ | 2 | `valueType === "Color"` && !dictionary | colorStrategy | Color picker, multivalue colors |
72
+ | 3 | `valueType === "Boolean"` | booleanStrategy | Checkbox/switch |
73
+ | 4 | `dictionary === true` | dictionaryStrategy | Select dropdowns with localized options |
74
+ | 5 | (default) | regularStrategy | ShortText, LongText, Number, Integer, DateTime |
71
75
 
72
- ### Value Setting
76
+ ### Value Getting
73
77
 
74
- `setPropertyValue` checks property type in order:
75
- 1. **Measure** (`valueType === "Measure"`): Creates a single value entry with `unitOfMeasureId`.
76
- 2. **Color** (`valueType === "Color"`, no dictionary): Creates value entry/entries with `colorCode`.
77
- 3. **Dictionary**: Resolves the selected dictionary item(s), expanding `localizedValues` into per-locale value entries for multilanguage dictionaries.
78
- 4. **Regular**: Handles the remaining cases -- simple text, number, boolean, datetime.
78
+ Each strategy's `get()` reads from `property.values` without mutation:
79
79
 
80
- ### Dictionary Localization
80
+ - **Multilanguage**: Finds value matching locale, falls back to value without languageCode
81
+ - **Multivalue**: Returns full array (filtered by locale for multilanguage)
82
+ - **Dictionary**: Returns `valueId` instead of `value`
83
+ - **Boolean**: Returns `false` (not `""`) when no value exists
81
84
 
82
- When `loadDictionaries` is called with a locale, each dictionary item's `localizedValues` array is searched for the matching locale. The localized display value replaces the generic `alias` for UI rendering. This is why the function returns `Promise<TPropertyDictionaryItem[]>` -- the items are cloned and enriched.
85
+ ### Value Setting
83
86
 
84
- ## Recipe: Rendering a Dynamic Property Form
87
+ Each strategy's `set()` writes to `property.values`:
85
88
 
86
- ```vue
87
- <script setup lang="ts">
88
- import { useDynamicProperties, useLanguages } from '@vc-shell/framework';
89
- import { ref, watch } from 'vue';
90
-
91
- const { currentLocale } = useLanguages();
92
- const { loading, loadDictionaries, getPropertyValue, setPropertyValue } =
93
- useDynamicProperties(searchDictionaryItems, PropertyValue, PropertyDictionaryItem);
94
-
95
- const properties = ref<Property[]>([]);
96
-
97
- // Display values, keyed by property ID
98
- function displayValue(property: Property) {
99
- return getPropertyValue(property, currentLocale.value);
100
- }
101
-
102
- // Handle value change from a form input
103
- async function onValueChange(property: Property, newValue: string) {
104
- if (property.dictionary) {
105
- const dictItems = await loadDictionaries(property.id!, '', currentLocale.value);
106
- setPropertyValue({
107
- property,
108
- value: newValue,
109
- dictionary: dictItems ?? [],
110
- locale: currentLocale.value,
111
- });
112
- } else {
113
- setPropertyValue({
114
- property,
115
- value: newValue,
116
- locale: currentLocale.value,
117
- });
118
- }
119
- }
120
- </script>
121
- ```
89
+ - **Empty values are cleaned up automatically** via `cleanEmptyValues()`. When user clears a field, scaffolding objects are removed and `property.values` becomes `[]`. This prevents false modification detection in `useBladeForm`.
90
+ - **Multilanguage cleanup**: Only the value for the current locale is removed; other locales are preserved.
91
+ - **Dictionary**: Expands `localizedValues` into per-locale value entries.
122
92
 
123
- ## Recipe: Dictionary Property with Search-as-You-Type
93
+ ### Empty Value Cleanup (cleanEmptyValues)
124
94
 
125
- ```vue
126
- <script setup lang="ts">
127
- import { useDynamicProperties } from '@vc-shell/framework';
128
- import { ref } from 'vue';
95
+ When `setPropertyValue` receives an empty value (`""`, `null`, `undefined`), it cleans up `property.values` instead of leaving scaffolding objects:
129
96
 
130
- const { loadDictionaries } = useDynamicProperties(/* ... */);
97
+ ```
98
+ Before: values: [{ value: "", languageCode: "en", propertyId: "abc", ... }]
99
+ After: values: []
100
+ ```
131
101
 
132
- const dictOptions = ref<PropertyDictionaryItem[]>([]);
102
+ This ensures that `useBladeForm`'s deep comparison correctly detects "no change" when user clears a field that was originally empty.
133
103
 
134
- async function onDictionarySearch(property: Property, keyword: string, locale: string) {
135
- const items = await loadDictionaries(property.id!, keyword, locale);
136
- dictOptions.value = items ?? [];
137
- }
138
- </script>
104
+ ## Recipe: With VcDynamicProperty
139
105
 
106
+ ```vue
140
107
  <template>
141
- <VcSelect
142
- :options="dictOptions"
143
- option-value="id"
144
- option-label="value"
145
- @search="(keyword) => onDictionarySearch(property, keyword, currentLocale)"
146
- @update:model-value="(val) => onValueChange(property, val)"
108
+ <VcDynamicProperty
109
+ v-for="property in properties"
110
+ :key="property.id"
111
+ :property="property"
112
+ :model-value="getPropertyValue(property, currentLocale)"
113
+ :options-getter="loadDictionaries"
114
+ :measurements-getter="loadMeasurements"
115
+ :current-language="currentLocale"
116
+ :value-type="property.valueType ?? ''"
117
+ :dictionary="property.dictionary"
118
+ :multivalue="property.multivalue"
119
+ :multilanguage="property.multilanguage"
120
+ @update:model-value="(ev) => setPropertyValue({ property, ...ev })"
147
121
  />
148
122
  </template>
123
+
124
+ <script setup lang="ts">
125
+ import { useDynamicProperties } from "@vc-shell/framework";
126
+
127
+ const { getPropertyValue, setPropertyValue, loadDictionaries, loadMeasurements } =
128
+ useDynamicProperties({
129
+ searchDictionary: searchDictionaryItems,
130
+ searchMeasurements: searchMeasurementItems,
131
+ });
132
+ </script>
149
133
  ```
150
134
 
151
135
  ## Tips
152
136
 
153
- - **`setPropertyValue` mutates the property in place.** Unlike `useAssets` which returns new arrays, this composable modifies `property.values` directly. This is by design because dynamic properties are typically part of a larger entity object that gets saved as a whole.
154
- - **Always pass `dictionary` when setting dictionary values.** Without the dictionary items, the composable cannot resolve `valueId` to the correct alias and localized values. Omitting it results in incomplete value entries.
155
- - **Empty value handling depends on `initialProp`.** When the user clears a field, the behavior differs based on whether the property originally had a value. If it did, clearing sets `values: []` (explicit empty). If it was always empty, a placeholder value entry is preserved. This distinction matters for the platform's diff/save logic.
156
- - **Boolean properties always have a value entry.** Unlike other types where clearing removes the value, boolean properties always maintain a value entry (with `value: false`). This ensures checkboxes render correctly.
157
- - **The composable is generic but not abstract.** You must pass concrete constructor classes, not interfaces. The constructors are called with `new` to create value and dictionary item instances.
137
+ - **`setPropertyValue` mutates the property in place.** `property.values` is modified directly. This is by design because dynamic properties are typically part of a larger entity object saved as a whole.
138
+ - **Always pass `dictionary` when setting dictionary values.** Without dictionary items, the composable cannot resolve `valueId` to the correct alias and localized values.
139
+ - **Boolean properties always have a value entry.** Unlike other types where clearing removes the value, boolean properties maintain a value entry (`value: false`). This ensures checkboxes render correctly.
140
+ - **No class constructors needed.** Values are created as plain objects via `createValue()`. No factory classes required.
141
+
142
+ ## File Structure
143
+
144
+ ```
145
+ useDynamicProperties/
146
+ index.ts — composable entry point (~60 lines)
147
+ types.ts — all interfaces
148
+ utils.ts — isEmptyValue, createValue, cleanEmptyValues, type guards
149
+ strategies/
150
+ index.ts — resolveStrategy() registry
151
+ types.ts — PropertyValueStrategy interface
152
+ regular.ts — ShortText, LongText, Number, Integer, DateTime
153
+ boolean.ts — Boolean
154
+ dictionary.ts — Dictionary (all multi/single × language/value)
155
+ measure.ts — Measure
156
+ color.ts — Color
157
+ ```
158
158
 
159
159
  ## Related
160
160
 
161
- - [useLanguages](../useLanguages/useLanguages.docs.md) -- locale management for multilanguage property rendering
162
- - `IBaseProperty`, `IBasePropertyValue` -- base interfaces defined in the composable file
163
- - `PropertyValue`, `PropertyDictionaryItem` from `@core/api/platform` -- concrete types for the standard platform
161
+ - [useBladeForm](../useBladeForm/useBladeForm.docs.md) form state management that uses `semanticEqual` for modification detection. `cleanEmptyValues` ensures compatibility.
162
+ - [VcDynamicProperty](../../../../ui/components/organisms/vc-dynamic-property/vc-dynamic-property.docs.md) UI component that renders dynamic properties
@@ -1,6 +1,6 @@
1
1
  # useSlowNetworkDetection
2
2
 
3
- Detects slow network conditions and shows a persistent warning notification so users know why the UI is unresponsive. Two detection channels work together: a **proactive** channel reads `navigator.connection.effectiveType` to catch weak connections before any request is made, and a **reactive** channel flags API requests that have been pending for more than 5 seconds. The notification auto-dismisses with a 3-second delay after conditions clear, preventing flicker. When the browser goes fully offline, the slow-network notification is suppressed in favor of the existing offline notification from `useConnectionStatus`.
3
+ Detects slow network conditions and shows a persistent warning notification so users know why the UI is unresponsive. Two detection channels work together: a **proactive** channel reads `navigator.connection.effectiveType` to catch weak connections before any request is made, and a **reactive** channel flags API requests that have been pending for more than 10 seconds. The notification auto-dismisses with a 3-second delay after conditions clear, preventing flicker. When the browser goes fully offline, the slow-network notification is suppressed in favor of the existing offline notification from `useConnectionStatus`.
4
4
 
5
5
  Like `useConnectionStatus`, this is a module-level singleton — calling it from multiple components shares the same state and listeners.
6
6
 
@@ -41,14 +41,14 @@ const { isSlowNetwork } = useSlowNetworkDetection();
41
41
  | Property | Type | Description |
42
42
  |---|---|---|
43
43
  | `isSlowNetwork` | `Readonly<Ref<boolean>>` | `true` when the network is slow (either channel active). Read-only. |
44
- | `trackRequest` | `(id: string) => void` | Start tracking a request. If it isn't untracked within 5 s, it counts as slow. |
44
+ | `trackRequest` | `(id: string) => void` | Start tracking a request. If it isn't untracked within 10 s, it counts as slow. |
45
45
  | `untrackRequest` | `(id: string) => void` | Stop tracking a request. Cancels the timer or decrements the slow count. |
46
46
 
47
47
  ### Constants
48
48
 
49
49
  | Name | Value | Purpose |
50
50
  |---|---|---|
51
- | `SLOW_REQUEST_THRESHOLD_MS` | `5000` | Time before a pending request is considered slow |
51
+ | `SLOW_REQUEST_THRESHOLD_MS` | `10000` | Time before a pending request is considered slow |
52
52
  | `DISMISS_DELAY_MS` | `3000` | Delay before hiding the notification after recovery |
53
53
  | `SLOW_EFFECTIVE_TYPES` | `["slow-2g", "2g"]` | Connection types flagged as slow |
54
54
 
@@ -60,7 +60,7 @@ On first call, the composable checks `navigator.connection.effectiveType` (Netwo
60
60
 
61
61
  ### Channel 2: Request timers (reactive)
62
62
 
63
- The fetch interceptor in `framework/core/interceptors/index.ts` calls `trackRequest(id)` before every `/api/*` request and `untrackRequest(id)` in the `finally` block. Each tracked request gets a 5-second timer. If the response arrives in time, the timer is cancelled. If not, `isSlowNetwork` becomes `true` and stays `true` until all slow requests complete.
63
+ The fetch interceptor in `framework/core/interceptors/index.ts` calls `trackRequest(id)` before every `/api/*` request and `untrackRequest(id)` in the `finally` block. Each tracked request gets a 10-second timer. If the response arrives in time, the timer is cancelled. If not, `isSlowNetwork` becomes `true` and stays `true` until all slow requests complete.
64
64
 
65
65
  ### Notification lifecycle
66
66
 
@@ -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
@@ -28,6 +28,7 @@ An image display component with predefined sizes, aspect ratio control, and a pl
28
28
  | `clickable` | `boolean` | `false` | Makes the image interactive with cursor and click event |
29
29
  | `emptyIcon` | `string` | `"lucide-image"` | Icon shown when `src` is empty |
30
30
  | `alt` | `string` | — | Accessible alt text |
31
+ | `thumbnailSize` | `ThumbnailSize` | — | Load a thumbnail variant instead of full-size image. Values: `"sm"`, `"md"`, `"lg"`, `"64x64"`, `"128x128"`, `"168x168"`, `"216x216"`, `"348x348"` |
31
32
 
32
33
  ## Size Reference
33
34
 
@@ -40,6 +40,7 @@ function deleteImage() { /* remove from list */ }
40
40
  | `name` | `string` | — | File name displayed in the tray |
41
41
  | `imageFit` | `"contain" \| "cover"` | `"contain"` | How the image fits within the tile |
42
42
  | `actions` | `VcImageTileActions` | — | Which built-in action buttons to show |
43
+ | `thumbnailSize` | `ThumbnailSize` | — | Load a thumbnail variant instead of full-size image |
43
44
 
44
45
  ## VcImageTileActions Interface
45
46
 
@@ -62,7 +62,7 @@ A blade has four visual zones, rendered top-to-bottom:
62
62
  |--------------------------------------|
63
63
  | [Save] [Delete] [Refresh] [More >] | <-- Toolbar
64
64
  |--------------------------------------|
65
- | ** Unsaved changes banner ** | <-- Status Banners
65
+ | ** Status banners (stacked) ** | <-- Status Banners
66
66
  |--------------------------------------|
67
67
  | |
68
68
  | Content (default slot) | <-- Content Area
@@ -74,7 +74,7 @@ A blade has four visual zones, rendered top-to-bottom:
74
74
 
75
75
  **Toolbar** -- Action buttons from the `toolbarItems` prop. Overflow items automatically collapse into a "More" dropdown (via `ResizeObserver`).
76
76
 
77
- **Status Banners** -- Yellow banner when `modified` is `true`; red banner when the blade has an error (set via `setError()`).
77
+ **Status Banners** -- Unified, priority-sorted banner area. System banners: yellow when `modified` is `true`, red when the blade has an error (via `setError()`). Custom banners can be added programmatically via `useBlade().addBanner()` — see [useBlade docs](../../../core/composables/useBlade/useBlade.docs.md#banner-management).
78
78
 
79
79
  **Content Area** -- The `default` slot. Scrolls independently of header and toolbar.
80
80
 
@@ -399,6 +399,31 @@ import { useBeforeUnload } from "@vc-shell/framework";
399
399
  useBeforeUnload(hasChanges); // Browser tab close warning
400
400
  ```
401
401
 
402
+ ## Custom Banners
403
+
404
+ Add informational, warning, or success banners to a blade programmatically. Banners appear between the header and toolbar, sorted by severity.
405
+
406
+ ```vue
407
+ <script setup lang="ts">
408
+ import { useBlade } from "@vc-shell/framework";
409
+
410
+ const { addBanner, removeBanner, clearBanners } = useBlade();
411
+
412
+ // Info banner (e.g. read-only mode)
413
+ addBanner({ variant: "info", message: "This record is read-only" });
414
+
415
+ // Dismissible warning with action
416
+ addBanner({
417
+ variant: "warning",
418
+ message: "License expires in 7 days",
419
+ dismissible: true,
420
+ action: { label: "Renew", handler: () => openRenewal() },
421
+ });
422
+ </script>
423
+ ```
424
+
425
+ Four variants are available: `danger`, `warning`, `info`, `success`. System banners (error and unsaved changes) are always present and cannot be removed by `clearBanners()`. For the full API reference, see [useBlade — Banner Management](../../../core/composables/useBlade/useBlade.docs.md#banner-management).
426
+
402
427
  ## Blade Width Control
403
428
 
404
429
  ```vue
@@ -13,17 +13,21 @@ A responsive multi-image gallery with drag-and-drop reorder, file upload, lightb
13
13
 
14
14
  | Prop | Type | Default | Description |
15
15
  |------|------|---------|-------------|
16
+ | `layout` | `"filmstrip" \| "grid"` | `"filmstrip"` | Layout mode — filmstrip shows a single scrollable row with expand/collapse; grid shows the classic multi-row auto-fill layout. |
17
+ | `label` | `string` | `undefined` | Label text displayed in the gallery header. |
18
+ | `required` | `boolean` | `false` | Shows a required indicator (`*`) on the label. |
16
19
  | `images` | `ICommonAsset[]` | `[]` | Array of image assets to display. |
17
20
  | `disabled` | `boolean` | `false` | Disables all interactive actions. |
18
21
  | `multiple` | `boolean` | `false` | Allow selecting multiple files in upload dialog. |
19
- | `loading` | `boolean` | `false` | Shows a loading spinner on the upload zone. |
22
+ | `loading` | `boolean` | `false` | Shows a loading overlay with spinner on the gallery. |
20
23
  | `itemActions` | `{ preview?: boolean; edit?: boolean; remove?: boolean }` | `{ preview: true, edit: true, remove: true }` | Per-tile action visibility. |
21
24
  | `rules` | `IValidationRules` | `undefined` | Validation rules for uploaded files. |
22
25
  | `name` | `string` | `"Gallery"` | Field name for validation messages. |
23
26
  | `accept` | `string` | `undefined` | Accepted file extensions. |
24
- | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Tile size preset. |
27
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Tile size preset. Sizes are smaller on mobile. |
25
28
  | `gap` | `number` | `8` | Gap between tiles in pixels. |
26
29
  | `imagefit` | `"contain" \| "cover"` | `"contain"` | How images fit within tiles. |
30
+ | `thumbnailSize` | `ThumbnailSize` | auto from `size` | Thumbnail size for tile images. Auto-mapped: sm→128x128, md→216x216, lg→348x348. Preview thumbnails use 64x64. |
27
31
 
28
32
  ## Events
29
33
 
@@ -44,20 +48,24 @@ A responsive multi-image gallery with drag-and-drop reorder, file upload, lightb
44
48
 
45
49
  ## Features
46
50
 
47
- - **Drag-and-drop reorder** -- Drag tiles to reorder. Emits `sort` with the new array. The dragged tile shows a ghost preview during the drag operation.
48
- - **External file drop** -- Drop files from the OS onto the gallery to upload. The entire gallery acts as a drop target with visual feedback.
49
- - **Lightbox preview** -- Click a tile to open a full-screen preview carousel. Navigate between images with arrow keys or swipe gestures.
50
- - **Responsive grid** -- Auto-fill grid adapts to container width using CSS grid with `auto-fill` and `minmax()`.
51
- - **Per-tile actions** -- Each tile shows preview, edit, and remove buttons on hover. Disable individual actions via the `itemActions` prop.
51
+ - **Filmstrip layout (default)** -- Single-row scrollable strip powered by Swiper. Navigate with arrows, mouse wheel, or swipe. Click "Expand (N)" to show all images in a grid. "Collapse" returns to filmstrip.
52
+ - **Grid layout** -- Classic auto-fill grid that wraps images into multiple rows. Set `layout="grid"` to use this mode.
53
+ - **Drag-and-drop reorder** -- Drag tiles by the grip handle to reorder (powered by SortableJS). Works on both desktop and touch devices. In filmstrip mode, dragging to the edge auto-scrolls the strip. Emits `sort` with the new array.
54
+ - **External file drop** -- Drop files from the OS onto the gallery to upload. The dashed border acts as a visual drop zone indicator (desktop only). A full overlay appears during drag-over.
55
+ - **Fullscreen preview** -- Click the preview button to open a fullscreen carousel. Swipe or use arrow keys to navigate. Thumbnail strip at the bottom syncs with the main image.
56
+ - **Per-tile actions** -- Each tile shows preview, edit, and remove buttons. On desktop: visible on hover. On mobile: visible on tap. Tile name is shown in the top bar alongside the drag handle.
57
+ - **Loading state** -- When `loading` is true, a pulsing border and spinner overlay appear on the gallery. Upload button is disabled. Swiper navigation is frozen.
58
+ - **Mobile responsive** -- Smaller tile sizes, no drag-and-drop hints, no dashed borders, compact action buttons, tap-to-reveal overlays. Uses `useResponsive` throughout.
59
+ - **Lazy loading** -- Images use native `loading="lazy"` for deferred loading.
52
60
 
53
61
  ## Basic Usage
54
62
 
55
63
  ```vue
56
64
  <VcGallery
65
+ label="Product Images"
66
+ required
57
67
  :images="product.images"
58
- size="md"
59
68
  imagefit="cover"
60
- :item-actions="{ preview: true, edit: true, remove: true }"
61
69
  @upload="handleUpload"
62
70
  @sort="handleSort"
63
71
  @edit="handleEdit"
@@ -65,6 +73,31 @@ A responsive multi-image gallery with drag-and-drop reorder, file upload, lightb
65
73
  />
66
74
  ```
67
75
 
76
+ ## Filmstrip Layout (Default)
77
+
78
+ ```vue
79
+ <VcGallery
80
+ label="Images"
81
+ :images="product.images"
82
+ imagefit="cover"
83
+ @upload="handleUpload"
84
+ @sort="handleSort"
85
+ @remove="handleRemove"
86
+ />
87
+ ```
88
+
89
+ ## Classic Grid Layout
90
+
91
+ ```vue
92
+ <VcGallery
93
+ layout="grid"
94
+ label="Attachments"
95
+ :images="product.images"
96
+ @upload="handleUpload"
97
+ @sort="handleSort"
98
+ />
99
+ ```
100
+
68
101
  ## Recipe: Product Image Gallery in a Blade
69
102
 
70
103
  ```vue
@@ -99,6 +132,8 @@ function handleRemove(image: ICommonAsset) {
99
132
  <template>
100
133
  <VcBlade title="Product Images">
101
134
  <VcGallery
135
+ label="Product Images"
136
+ required
102
137
  :images="images"
103
138
  multiple
104
139
  accept=".jpg,.png,.webp"
@@ -152,20 +187,24 @@ function handleRemove(image: ICommonAsset) {
152
187
 
153
188
  ## Tips
154
189
 
155
- - The `size` prop controls tile dimensions: `"sm"` is good for compact grids (e.g., thumbnails in a sidebar), `"md"` is the standard, and `"lg"` works well for hero image management.
190
+ - The `size` prop controls tile dimensions: `"sm"` is good for compact grids (e.g., thumbnails in a sidebar), `"md"` is the standard, and `"lg"` works well for hero image management. Tiles are automatically smaller on mobile.
156
191
  - Use `imagefit="cover"` for photo galleries where cropping is acceptable, and `"contain"` for logos or icons where the full image must be visible.
157
- - The upload zone tile always appears at the end of the grid when the gallery is not disabled. It accepts both click and drag-and-drop interactions.
192
+ - The filmstrip layout is ideal for blades where vertical space is limited. Users can expand to see all images or scroll horizontally. The classic grid layout is better for dedicated media management pages.
193
+ - Use the `label` prop to display a header label integrated with the upload button. Add `required` to show a required indicator.
158
194
  - The `startingSortOrder` parameter in the `upload` event tells you where the new files should be inserted in the sort order. Use it to maintain correct ordering when appending new images.
195
+ - On desktop, reorder by dragging the grip handle icon. In filmstrip mode, dragging to the strip edge auto-scrolls to reveal more tiles.
196
+ - On mobile, tap a tile to reveal action buttons and the image name. Drag-and-drop hints and dashed borders are hidden automatically.
159
197
 
160
198
  ## Accessibility
161
199
 
162
200
  - Tiles are keyboard-navigable with Tab and action buttons are focusable
163
- - Lightbox preview supports keyboard navigation (arrow keys, Escape to close)
164
- - The upload zone is accessible via keyboard (Enter/Space to open file picker)
165
- - Drag-and-drop reorder requires mouse/touch; provide an alternative reorder mechanism for keyboard-only users if needed
201
+ - Fullscreen preview supports keyboard navigation (arrow keys, Escape to close) and swipe gestures
202
+ - The upload button in the header is keyboard-accessible
203
+ - On mobile, tile actions are revealed via tap with click-outside to dismiss
166
204
 
167
205
  ## Related Components
168
206
 
169
207
  - **VcImageUpload** -- single-image upload component
170
- - **VcImageTile** -- the internal tile component used for each image
171
- - **VcLabel** / **VcHint** -- use alongside VcGallery for field labeling
208
+ - **VcImageTile** -- the internal tile component used for each image (topbar with name + drag handle, bottom tray with actions)
209
+ - **VcFileUpload** -- the file upload drop zone used in empty gallery state
210
+ - **VcLabel** -- used internally when `label` prop is set
@@ -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,102 +0,0 @@
1
- # ChangePasswordPage
2
-
3
- Change password page with current, new, and confirm password fields. Supports a `forced` mode for expired passwords that displays an info banner and is triggered by post-login redirect. This full-page variant is used when the user must change their password before accessing the application (e.g., expired password policy). For voluntary password changes from within the app, the `ChangePasswordButton` in the settings menu opens a popup instead.
4
-
5
- ## When to Use
6
-
7
- - When a signed-in user wants to change their password (full-page flow)
8
- - In `forced` mode after login when the user's password has expired
9
- - The standard vc-shell routing maps `/change-password` to this page
10
-
11
- ## Basic Usage
12
-
13
- ```vue
14
- <template>
15
- <ChangePassword />
16
- </template>
17
-
18
- <!-- Forced mode (expired password) -->
19
- <template>
20
- <ChangePassword forced />
21
- </template>
22
- ```
23
-
24
- With custom branding:
25
-
26
- ```vue
27
- <template>
28
- <ChangePassword
29
- forced
30
- logo="/assets/my-company-logo.svg"
31
- background="/assets/custom-background.jpg"
32
- />
33
- </template>
34
- ```
35
-
36
- ## Key Props
37
-
38
- | Prop | Type | Default | Description |
39
- |------|------|---------|-------------|
40
- | `forced` | `boolean` | `false` | Show expired-password info banner and adjusted title |
41
- | `logo` | `string` | - | Override logo image URL |
42
- | `background` | `string` | - | Custom background image URL |
43
-
44
- ## Recipe: Router Configuration with Forced Mode
45
-
46
- Set up the route so the login page can redirect here when the password is expired:
47
-
48
- ```ts
49
- import ChangePassword from "@vc-shell/framework/shared/pages/ChangePasswordPage";
50
-
51
- const routes = [
52
- {
53
- path: "/change-password",
54
- name: "ChangePassword",
55
- component: ChangePassword,
56
- props: (route) => ({
57
- forced: route.query.forced === "true",
58
- }),
59
- },
60
- ];
61
- ```
62
-
63
- In the login flow, redirect when the user's password has expired:
64
-
65
- ```ts
66
- async function handleLogin() {
67
- const result = await signIn(username.value, password.value);
68
- if (result.passwordExpired) {
69
- router.push({ name: "ChangePassword", query: { forced: "true" } });
70
- } else {
71
- router.push("/");
72
- }
73
- }
74
- ```
75
-
76
- ## Features
77
-
78
- - **Real-time password policy validation**: Uses `useUserManagement().validatePassword` to check the new password against the platform's policy as the user types (minimum length, required uppercase, lowercase, digits, special characters)
79
- - **Equal password detection**: Shows a specific "Equal-passwords" error when the new password matches the current password, without making an API call
80
- - **Confirm-password mismatch detection**: Validates that the new password and confirm password fields match
81
- - **Forced mode banner**: When `forced` is `true`, displays an info banner explaining that the password has expired and must be changed
82
- - **Cancel behavior**: Cancel button signs out the user and redirects to `/login`
83
- - **Success redirect**: On successful password change, redirects to `/` (main application)
84
-
85
- ## Details
86
-
87
- - **Auth layout**: Renders inside `VcAuthLayout`, providing the centered card design with logo and background.
88
- - **Password policy**: The validation rules are fetched from the platform API and include configurable requirements (minimum length, character classes, etc.). The component displays these rules as a checklist.
89
- - **Forced vs voluntary**: In forced mode (`forced=true`), the page title changes to reflect the expired password scenario, and an info banner explains why the change is required. The form fields and behavior are otherwise identical.
90
- - **Sign-out on cancel**: If the user cancels during a forced password change, they are signed out. This prevents access to the application with an expired password.
91
-
92
- ## Tips
93
-
94
- - The `forced` prop is typically set via a route query parameter, not hardcoded. The login page detects expired passwords and redirects with the appropriate flag.
95
- - Password policy validation runs on keyup, providing immediate feedback. The submit button is disabled until all policy requirements are met and the passwords match.
96
- - This page is distinct from the `ChangePasswordButton` popup. The page is for full-screen flow (forced changes), while the popup is for voluntary in-app password changes.
97
-
98
- ## Related Components
99
-
100
- - **VcAuthLayout** - The underlying centered card layout
101
- - **LoginPage** - Redirects here when `user.passwordExpired` is true
102
- - **ChangePasswordButton** - Settings menu popup variant for voluntary password changes