@vc-shell/vc-app-skill 2.0.0-alpha.30 → 2.0.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/runtime/VERSION +1 -1
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/composables/useDynamicProperties/useDynamicProperties.docs.md +104 -105
- package/runtime/knowledge/docs/core/composables/useSlowNetworkDetection/useSlowNetworkDetection.docs.md +4 -4
- package/runtime/knowledge/docs/ui/components/atoms/vc-image/vc-image.docs.md +1 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-image-tile/vc-image-tile.docs.md +1 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-blade/vc-blade.docs.md +27 -2
- package/runtime/knowledge/docs/ui/components/organisms/vc-gallery/vc-gallery.docs.md +55 -16
- package/runtime/knowledge/docs/core/composables/useBladeContext.docs.md +0 -111
- package/runtime/knowledge/docs/core/composables/useBladeWidgets.docs.md +0 -305
- package/runtime/knowledge/docs/core/composables/useGlobalSearch/useGlobalSearch.docs.md +0 -146
- package/runtime/knowledge/docs/core/composables/useMenuExpanded.docs.md +0 -83
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# [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)
|
|
2
|
+
|
|
3
|
+
**Note:** Version bump only for package @vc-shell/vc-app-skill
|
|
4
|
+
|
|
1
5
|
# [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
6
|
|
|
3
7
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vc-shell/vc-app-skill",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.31",
|
|
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.
|
|
1
|
+
2.0.0-alpha.31
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Synced from framework at commit
|
|
1
|
+
Synced from framework at commit 2f71da38b on 2026-04-01T16:03:16.012Z
|
package/runtime/knowledge/docs/core/composables/useDynamicProperties/useDynamicProperties.docs.md
CHANGED
|
@@ -1,163 +1,162 @@
|
|
|
1
1
|
# useDynamicProperties
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Composable for managing dynamic (runtime-defined) property values with support for multilanguage, multivalue, dictionary, color, and measurement property types.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
(
|
|
23
|
-
|
|
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
|
-
###
|
|
28
|
+
### Options
|
|
32
29
|
|
|
33
|
-
|
|
|
34
|
-
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
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
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
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` | `
|
|
55
|
-
| `value` | `string \|
|
|
56
|
-
| `dictionary` | `
|
|
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
|
-
###
|
|
64
|
+
### Strategy Resolution
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
The composable resolves a strategy handler based on property flags:
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
76
|
+
### Value Getting
|
|
73
77
|
|
|
74
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
### Value Setting
|
|
83
86
|
|
|
84
|
-
|
|
87
|
+
Each strategy's `set()` writes to `property.values`:
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
+
### Empty Value Cleanup (cleanEmptyValues)
|
|
124
94
|
|
|
125
|
-
|
|
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
|
-
|
|
97
|
+
```
|
|
98
|
+
Before: values: [{ value: "", languageCode: "en", propertyId: "abc", ... }]
|
|
99
|
+
After: values: []
|
|
100
|
+
```
|
|
131
101
|
|
|
132
|
-
|
|
102
|
+
This ensures that `useBladeForm`'s deep comparison correctly detects "no change" when user clears a field that was originally empty.
|
|
133
103
|
|
|
134
|
-
|
|
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
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.**
|
|
154
|
-
- **Always pass `dictionary` when setting dictionary values.** Without
|
|
155
|
-
- **
|
|
156
|
-
- **
|
|
157
|
-
|
|
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
|
-
- [
|
|
162
|
-
-
|
|
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
|
|
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
|
|
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` | `
|
|
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
|
|
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
|
|
|
@@ -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
|
-
| **
|
|
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** --
|
|
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
|
|
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
|
-
- **
|
|
48
|
-
- **
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
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
|
|
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
|
-
-
|
|
164
|
-
- The upload
|
|
165
|
-
-
|
|
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
|
-
- **
|
|
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,111 +0,0 @@
|
|
|
1
|
-
# useBladeContext (defineBladeContext / injectBladeContext)
|
|
2
|
-
|
|
3
|
-
Exposes blade-level data to descendant widgets, extensions, and nested components via Vue's provide/inject mechanism. This pair of functions eliminates the need for prop drilling when child widgets or extension points need access to the parent blade's entity data, loading flags, or other shared state.
|
|
4
|
-
|
|
5
|
-
The pattern follows a "define once, inject anywhere" approach: the blade component calls `defineBladeContext` during setup, and any descendant (no matter how deeply nested) can call `injectBladeContext` to read that data reactively.
|
|
6
|
-
|
|
7
|
-
## When to Use
|
|
8
|
-
|
|
9
|
-
- Share blade state (current entity, loading flags, disabled state) with child widgets without prop drilling
|
|
10
|
-
- Access parent blade data from an extension or widget component
|
|
11
|
-
- Expose selective fields to widgets (e.g., only the entity ID) via a computed getter
|
|
12
|
-
- When NOT to use: for cross-blade communication between sibling blades (use `useBlade` / blade messaging instead)
|
|
13
|
-
|
|
14
|
-
## Basic Usage
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
// In a blade's <script setup>
|
|
18
|
-
import { defineBladeContext, injectBladeContext } from '@vc-shell/framework';
|
|
19
|
-
|
|
20
|
-
// Provide context — refs/computeds are auto-unwrapped for consumers
|
|
21
|
-
defineBladeContext({ item, disabled, loading });
|
|
22
|
-
|
|
23
|
-
// Or with a computed for selective exposure
|
|
24
|
-
defineBladeContext(computed(() => ({ id: item.value?.id })));
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
```typescript
|
|
28
|
-
// In a widget or nested component
|
|
29
|
-
import { injectBladeContext } from '@vc-shell/framework';
|
|
30
|
-
|
|
31
|
-
const ctx = injectBladeContext();
|
|
32
|
-
// Refs are already unwrapped — access values directly, no .value needed
|
|
33
|
-
const entityId = computed(() => ctx.value.id as string);
|
|
34
|
-
const item = computed(() => ctx.value.item as { id: string; name: string });
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## API
|
|
38
|
-
|
|
39
|
-
### defineBladeContext
|
|
40
|
-
|
|
41
|
-
| Parameter | Type | Required | Description |
|
|
42
|
-
|---|---|---|---|
|
|
43
|
-
| `data` | `MaybeRefOrGetter<Record<string, unknown>>` | Yes | Plain object, ref, or getter to expose |
|
|
44
|
-
|
|
45
|
-
Returns `void`. Must be called in the blade's `<script setup>`.
|
|
46
|
-
|
|
47
|
-
### injectBladeContext
|
|
48
|
-
|
|
49
|
-
Takes no parameters. Returns `ComputedRef<Record<string, unknown>>`.
|
|
50
|
-
|
|
51
|
-
Throws `InjectionError` if no ancestor blade has called `defineBladeContext`.
|
|
52
|
-
|
|
53
|
-
## Recipe: Widget Consuming Blade Context
|
|
54
|
-
|
|
55
|
-
A typical pattern is a sidebar widget that needs to load related data based on the current blade entity. The widget does not receive any props from the blade -- it reads the entity ID from the blade context:
|
|
56
|
-
|
|
57
|
-
```vue
|
|
58
|
-
<script setup lang="ts">
|
|
59
|
-
// widgets/RelatedOrdersWidget.vue
|
|
60
|
-
import { computed, watch } from "vue";
|
|
61
|
-
import { injectBladeContext } from "@vc-shell/framework";
|
|
62
|
-
|
|
63
|
-
const ctx = injectBladeContext();
|
|
64
|
-
const customerId = computed(() => ctx.value.id as string | undefined);
|
|
65
|
-
|
|
66
|
-
// Reload orders whenever the customer changes
|
|
67
|
-
watch(customerId, async (id) => {
|
|
68
|
-
if (id) {
|
|
69
|
-
await loadOrders(id);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
</script>
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
```vue
|
|
76
|
-
<script setup lang="ts">
|
|
77
|
-
// blades/CustomerDetailBlade.vue
|
|
78
|
-
import { ref, computed } from "vue";
|
|
79
|
-
import { defineBladeContext } from "@vc-shell/framework";
|
|
80
|
-
|
|
81
|
-
const customer = ref({ id: "cust-1", name: "Acme Corp" });
|
|
82
|
-
const loading = ref(false);
|
|
83
|
-
|
|
84
|
-
// Expose the customer data to all descendant widgets
|
|
85
|
-
defineBladeContext(computed(() => ({
|
|
86
|
-
id: customer.value?.id,
|
|
87
|
-
name: customer.value?.name,
|
|
88
|
-
loading: loading.value,
|
|
89
|
-
})));
|
|
90
|
-
</script>
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Details
|
|
94
|
-
|
|
95
|
-
- **Automatic ref unwrapping**: `defineBladeContext` shallow-unwraps all ref/computed values in the provided object. Consumers get plain values directly (`ctx.value.item` instead of `ctx.value.item.value`). This works reactively — when the source ref changes, the context updates automatically.
|
|
96
|
-
- **Reactivity**: The provided context is always wrapped in a `computed`, so consumers receive a `ComputedRef` regardless of whether the provider passed a plain object, a ref, or a getter. Changes propagate automatically.
|
|
97
|
-
- **Injection key**: Uses `BladeContextKey` from `framework/injection-keys.ts`. This is a framework-level Symbol, so there is no risk of key collision with application code.
|
|
98
|
-
- **Error handling**: `injectBladeContext` throws an `InjectionError` with a descriptive message if called outside a blade component tree. This fails fast during development rather than silently returning `undefined`.
|
|
99
|
-
- **Scope**: The context is scoped to the Vue component subtree. Each blade in the stack has its own context, so nested blades do not leak data upward or sideways.
|
|
100
|
-
|
|
101
|
-
## Tips
|
|
102
|
-
|
|
103
|
-
- Prefer exposing a computed getter rather than the full reactive object when only a subset of fields is needed. This minimizes unnecessary re-renders in consuming widgets.
|
|
104
|
-
- The context value is untyped (`Record<string, unknown>`). Use type assertions or a typed wrapper in your module if you need type safety (e.g., `ctx.value.id as string`).
|
|
105
|
-
- If a blade does not call `defineBladeContext`, any descendant calling `injectBladeContext` will throw. Make sure all blades that host widgets define their context.
|
|
106
|
-
|
|
107
|
-
## Related
|
|
108
|
-
|
|
109
|
-
- `BladeContextKey` in `framework/injection-keys.ts`
|
|
110
|
-
- `useBladeWidgets` -- widgets that consume blade context
|
|
111
|
-
- `useBladeStack` -- manages the blade navigation stack
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
# useBladeWidgets / useWidgetTrigger
|
|
2
|
-
|
|
3
|
-
Two composables for the widget system — one for the **blade side**, one for the **widget side**.
|
|
4
|
-
|
|
5
|
-
| Composable | Called from | Purpose |
|
|
6
|
-
|---|---|---|
|
|
7
|
-
| `useBladeWidgets` | Blade component | Register headless widgets + get `refresh()` / `refreshAll()` |
|
|
8
|
-
| `useWidgetTrigger` | External widget component | Register trigger callbacks (`onRefresh`, `onClick`) via provide/inject |
|
|
9
|
-
|
|
10
|
-
Headless widgets are defined as plain configuration objects with reactive refs for dynamic values like badge counts and loading states. External component-based widgets use `useWidgetTrigger` to register their refresh callbacks so the hosting blade can trigger them.
|
|
11
|
-
|
|
12
|
-
## When to Use
|
|
13
|
-
|
|
14
|
-
- **`useBladeWidgets`**: Register sidebar widgets (counters, action buttons) for a blade without creating Vue components. Refresh widget data after blade operations (save, delete).
|
|
15
|
-
- **`useWidgetTrigger`**: Inside an external widget component (registered via `registerExternalWidget`) to register `onRefresh` / `onClick` callbacks. The blade can then call `refresh(widgetId)` or `refreshAll()` to trigger them.
|
|
16
|
-
- When NOT to use `useBladeWidgets`: for widgets that need their own template or complex UI (use `registerExternalWidget` + `useWidgetTrigger` instead).
|
|
17
|
-
|
|
18
|
-
## Basic Usage
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
import { useBladeWidgets } from '@vc-shell/framework';
|
|
22
|
-
|
|
23
|
-
const { refreshAll } = useBladeWidgets([
|
|
24
|
-
{
|
|
25
|
-
id: 'OffersWidget',
|
|
26
|
-
icon: 'lucide-tag',
|
|
27
|
-
title: 'OFFERS.TITLE',
|
|
28
|
-
badge: offersCount,
|
|
29
|
-
loading: offersLoading,
|
|
30
|
-
onClick: () => openBlade({ name: 'OffersList' }),
|
|
31
|
-
onRefresh: () => reloadOffers(),
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
id: 'ReviewsWidget',
|
|
35
|
-
icon: 'lucide-star',
|
|
36
|
-
title: 'REVIEWS.TITLE',
|
|
37
|
-
badge: reviewsCount,
|
|
38
|
-
isVisible: computed(() => !!item.value?.id),
|
|
39
|
-
onClick: () => openBlade({ name: 'ReviewsList' }),
|
|
40
|
-
},
|
|
41
|
-
]);
|
|
42
|
-
|
|
43
|
-
// After a save, refresh all widget data
|
|
44
|
-
await saveEntity();
|
|
45
|
-
refreshAll();
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## API
|
|
49
|
-
|
|
50
|
-
### Parameters
|
|
51
|
-
|
|
52
|
-
| Parameter | Type | Required | Description |
|
|
53
|
-
|---|---|---|---|
|
|
54
|
-
| `widgets` | `HeadlessWidgetDeclaration[]` | Yes | Array of widget declarations |
|
|
55
|
-
|
|
56
|
-
### HeadlessWidgetDeclaration
|
|
57
|
-
|
|
58
|
-
| Field | Type | Required | Description |
|
|
59
|
-
|---|---|---|---|
|
|
60
|
-
| `id` | `string` | Yes | Unique widget identifier |
|
|
61
|
-
| `icon` | `string` | Yes | Icon name (e.g., `"lucide-tag"`) |
|
|
62
|
-
| `title` | `string` | Yes | i18n key or display title |
|
|
63
|
-
| `badge` | `Ref<number \| string>` | No | Badge counter value |
|
|
64
|
-
| `loading` | `Ref<boolean>` | No | Show loading indicator |
|
|
65
|
-
| `disabled` | `Ref<boolean> \| boolean` | No | Disable the widget |
|
|
66
|
-
| `isVisible` | `ComputedRef<boolean> \| boolean` | No | Toggle visibility |
|
|
67
|
-
| `onClick` | `() => void` | No | Action when widget is clicked |
|
|
68
|
-
| `onRefresh` | `() => void \| Promise<void>` | No | Called by `refresh(id)` or `refreshAll()` |
|
|
69
|
-
|
|
70
|
-
### Returns
|
|
71
|
-
|
|
72
|
-
| Property | Type | Description |
|
|
73
|
-
|---|---|---|
|
|
74
|
-
| `refresh` | `(widgetId: string) => void` | Trigger `onRefresh` on a specific widget |
|
|
75
|
-
| `refreshAll` | `() => void` | Trigger `onRefresh` on all widgets that have one |
|
|
76
|
-
|
|
77
|
-
## useWidgetTrigger
|
|
78
|
-
|
|
79
|
-
Widget-side composable for external component-based widgets. Registers a trigger contract (`onRefresh`, `onClick`, `badge`) via provide/inject — no props, IDs, or service knowledge required.
|
|
80
|
-
|
|
81
|
-
### Basic Usage
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
import { useWidgetTrigger } from '@vc-shell/framework';
|
|
85
|
-
|
|
86
|
-
// Inside an external widget component:
|
|
87
|
-
useWidgetTrigger({ onRefresh: loadData });
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### IWidgetTrigger
|
|
91
|
-
|
|
92
|
-
| Field | Type | Required | Description |
|
|
93
|
-
|---|---|---|---|
|
|
94
|
-
| `icon` | `string` | No | Lucide icon name for dropdown rendering |
|
|
95
|
-
| `title` | `string` | No | Display title (fallback: widget's title) |
|
|
96
|
-
| `badge` | `Ref<number \| string>` | No | Reactive badge value |
|
|
97
|
-
| `onClick` | `() => void` | No | Handler called when widget is clicked in dropdown |
|
|
98
|
-
| `onRefresh` | `() => void \| Promise<void>` | No | Handler called to refresh widget data |
|
|
99
|
-
| `disabled` | `Ref<boolean> \| boolean` | No | Disabled state |
|
|
100
|
-
|
|
101
|
-
### How It Works
|
|
102
|
-
|
|
103
|
-
1. `WidgetContainer` wraps each component-based widget in a `WidgetScope` provider
|
|
104
|
-
2. `WidgetScope` provides a `setTrigger` function scoped to the specific widget ID and blade ID
|
|
105
|
-
3. `useWidgetTrigger` injects this scope and calls `setTrigger` — no props or IDs needed
|
|
106
|
-
4. When the blade calls `refresh(widgetId)` or `refreshAll()`, the registered `onRefresh` is invoked
|
|
107
|
-
|
|
108
|
-
## Recipe: External Widget with Refresh
|
|
109
|
-
|
|
110
|
-
A complete example of an external widget that shows an unread message count and supports refresh from the blade:
|
|
111
|
-
|
|
112
|
-
**1. Register the external widget (module index.ts):**
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
import { createAppModule, registerExternalWidget, BladeDescriptor } from "@vc-shell/framework";
|
|
116
|
-
import { markRaw } from "vue";
|
|
117
|
-
import { MessageWidget } from "./components/widgets";
|
|
118
|
-
|
|
119
|
-
registerExternalWidget({
|
|
120
|
-
id: "MessageWidget",
|
|
121
|
-
component: markRaw(MessageWidget),
|
|
122
|
-
targetBlades: ["ProductDetails", "OrderDetails"],
|
|
123
|
-
isVisible: (blade?: BladeDescriptor) => !!blade?.param,
|
|
124
|
-
});
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
**2. Widget component (message-widget.vue):**
|
|
128
|
-
|
|
129
|
-
```vue
|
|
130
|
-
<template>
|
|
131
|
-
<VcWidget
|
|
132
|
-
v-loading:500="loading"
|
|
133
|
-
:title="$t('MESSENGER.WIDGET.TITLE')"
|
|
134
|
-
icon="lucide-message-circle"
|
|
135
|
-
:value="messageCount"
|
|
136
|
-
@click="openMessageBlade"
|
|
137
|
-
/>
|
|
138
|
-
</template>
|
|
139
|
-
|
|
140
|
-
<script setup lang="ts">
|
|
141
|
-
import { ref, computed, onMounted } from "vue";
|
|
142
|
-
import {
|
|
143
|
-
loading as vLoading,
|
|
144
|
-
useBlade,
|
|
145
|
-
injectBladeContext,
|
|
146
|
-
useWidgetTrigger,
|
|
147
|
-
VcWidget,
|
|
148
|
-
} from "@vc-shell/framework";
|
|
149
|
-
|
|
150
|
-
const ctx = injectBladeContext();
|
|
151
|
-
const entityId = computed(() => (ctx.value.item as { id?: string })?.id ?? "");
|
|
152
|
-
|
|
153
|
-
const messageCount = ref(0);
|
|
154
|
-
const loading = ref(false);
|
|
155
|
-
|
|
156
|
-
const loadData = async () => {
|
|
157
|
-
loading.value = true;
|
|
158
|
-
try {
|
|
159
|
-
messageCount.value = await api.getUnreadCount(entityId.value);
|
|
160
|
-
} finally {
|
|
161
|
-
loading.value = false;
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
// Register refresh callback — blade can call refreshAll() after save
|
|
166
|
-
useWidgetTrigger({ onRefresh: loadData });
|
|
167
|
-
|
|
168
|
-
onMounted(() => {
|
|
169
|
-
if (entityId.value) loadData();
|
|
170
|
-
});
|
|
171
|
-
</script>
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
**3. Blade refreshes widgets after save:**
|
|
175
|
-
|
|
176
|
-
```vue
|
|
177
|
-
<script setup lang="ts">
|
|
178
|
-
import { useBladeWidgets } from "@vc-shell/framework";
|
|
179
|
-
|
|
180
|
-
// Empty array — blade doesn't register headless widgets,
|
|
181
|
-
// but gets refresh/refreshAll for external widgets
|
|
182
|
-
const { refresh, refreshAll } = useBladeWidgets([]);
|
|
183
|
-
|
|
184
|
-
async function save() {
|
|
185
|
-
await api.saveProduct(product.value);
|
|
186
|
-
refreshAll(); // refresh all widgets (including MessageWidget)
|
|
187
|
-
// or: refresh("MessageWidget"); // refresh a specific widget by ID
|
|
188
|
-
}
|
|
189
|
-
</script>
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
## Recipe: Product Detail Blade with Multiple Widgets
|
|
193
|
-
|
|
194
|
-
```vue
|
|
195
|
-
<script setup lang="ts">
|
|
196
|
-
import { ref, computed } from "vue";
|
|
197
|
-
import { useBladeWidgets, defineBladeContext } from "@vc-shell/framework";
|
|
198
|
-
|
|
199
|
-
const product = ref({ id: "prod-1", name: "Widget A" });
|
|
200
|
-
const offersCount = ref(0);
|
|
201
|
-
const reviewsCount = ref(0);
|
|
202
|
-
const offersLoading = ref(false);
|
|
203
|
-
|
|
204
|
-
// Expose product data to widgets
|
|
205
|
-
defineBladeContext(computed(() => ({ id: product.value?.id })));
|
|
206
|
-
|
|
207
|
-
async function reloadOffers() {
|
|
208
|
-
offersLoading.value = true;
|
|
209
|
-
try {
|
|
210
|
-
const result = await api.searchOffers({ productId: product.value.id });
|
|
211
|
-
offersCount.value = result.totalCount;
|
|
212
|
-
} finally {
|
|
213
|
-
offersLoading.value = false;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const { refreshAll } = useBladeWidgets([
|
|
218
|
-
{
|
|
219
|
-
id: "OffersWidget",
|
|
220
|
-
icon: "lucide-tag",
|
|
221
|
-
title: "PRODUCT.WIDGETS.OFFERS",
|
|
222
|
-
badge: offersCount,
|
|
223
|
-
loading: offersLoading,
|
|
224
|
-
isVisible: computed(() => !!product.value?.id),
|
|
225
|
-
onClick: () => openBlade({ name: "OffersList" }),
|
|
226
|
-
onRefresh: reloadOffers,
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
id: "ReviewsWidget",
|
|
230
|
-
icon: "lucide-star",
|
|
231
|
-
title: "PRODUCT.WIDGETS.REVIEWS",
|
|
232
|
-
badge: reviewsCount,
|
|
233
|
-
isVisible: computed(() => !!product.value?.id),
|
|
234
|
-
onClick: () => openBlade({ name: "ReviewsList" }),
|
|
235
|
-
},
|
|
236
|
-
]);
|
|
237
|
-
|
|
238
|
-
async function save() {
|
|
239
|
-
await api.saveProduct(product.value);
|
|
240
|
-
// Refresh all widget counts after saving
|
|
241
|
-
refreshAll();
|
|
242
|
-
}
|
|
243
|
-
</script>
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
## Prerequisites
|
|
247
|
-
|
|
248
|
-
**`useBladeWidgets`**:
|
|
249
|
-
- Must be called inside a blade component rendered by `VcBladeSlot` (requires `BladeDescriptorKey` injection).
|
|
250
|
-
- `WidgetService` must be provided in the component tree (automatically available in vc-shell apps).
|
|
251
|
-
- Calling outside a blade context throws an error with a descriptive message.
|
|
252
|
-
|
|
253
|
-
**`useWidgetTrigger`**:
|
|
254
|
-
- Must be called inside a widget component rendered by `WidgetContainer` (requires `WidgetScopeKey` injection).
|
|
255
|
-
- If called outside a widget scope, logs a warning and does nothing (does not throw).
|
|
256
|
-
|
|
257
|
-
## Details
|
|
258
|
-
|
|
259
|
-
- **Lifecycle management**: Widgets are registered in `onMounted` and unregistered in `onUnmounted`. This ensures the WidgetService always reflects the currently visible blades.
|
|
260
|
-
- **Blade ID resolution**: The composable injects `BladeDescriptorKey` to determine which blade the widgets belong to. Each blade has its own isolated widget list.
|
|
261
|
-
- **Trigger pattern**: The `onRefresh` callback is stored as a `trigger` on the registered widget. When `refresh(id)` or `refreshAll()` is called, the trigger is invoked. Widgets without `onRefresh` are silently skipped.
|
|
262
|
-
|
|
263
|
-
## Tips
|
|
264
|
-
|
|
265
|
-
- Use `refreshAll()` after any blade operation that might change widget badge counts (save, delete, import).
|
|
266
|
-
- The `badge` field accepts both numbers and strings. Use a string for non-numeric badges like "New" or "!".
|
|
267
|
-
- Keep widget IDs unique within a blade. Duplicate IDs will overwrite previous registrations.
|
|
268
|
-
- Combine with `defineBladeContext` to expose blade entity data that widget components (non-headless) can consume via `injectBladeContext`.
|
|
269
|
-
|
|
270
|
-
## Common Mistakes
|
|
271
|
-
|
|
272
|
-
### Calling useWidgetTrigger outside WidgetContainer scope
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
// Wrong — called in a standalone component, not rendered inside a blade widget slot
|
|
276
|
-
export default defineComponent({
|
|
277
|
-
setup() {
|
|
278
|
-
useWidgetTrigger({ onRefresh: loadData }); // ⚠️ Logs warning, trigger not registered
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
```typescript
|
|
284
|
-
// Correct — called inside a widget component registered via registerExternalWidget
|
|
285
|
-
// and rendered by WidgetContainer within a blade
|
|
286
|
-
useWidgetTrigger({ onRefresh: loadData }); // ✓ WidgetScope provides context
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
### Forgetting to pass empty array to useBladeWidgets for refresh-only usage
|
|
290
|
-
|
|
291
|
-
```typescript
|
|
292
|
-
// Wrong — useBladeWidgets requires an array argument
|
|
293
|
-
const { refreshAll } = useBladeWidgets(); // TS error
|
|
294
|
-
|
|
295
|
-
// Correct — pass empty array when you only need refresh/refreshAll
|
|
296
|
-
const { refreshAll } = useBladeWidgets([]);
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
## Related
|
|
300
|
-
|
|
301
|
-
- `defineBladeContext` / `injectBladeContext` -- expose/consume blade data in external widgets
|
|
302
|
-
- `registerExternalWidget` -- register a component-based widget globally for target blades
|
|
303
|
-
- `WidgetService` in `@core/services/widget-service` -- underlying service
|
|
304
|
-
- `WidgetScope` in `vc-blade/_internal/widgets/WidgetScope.vue` -- provides `WidgetScopeKey` to widget components
|
|
305
|
-
- `VcBladeSlot` -- the blade wrapper that provides `BladeDescriptorKey`
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
# useGlobalSearch
|
|
2
|
-
|
|
3
|
-
Provides access to the global search service state: per-blade search visibility, search queries, and methods to toggle/close search. The service maintains a map of blade IDs to search state, allowing each blade in the stack to have its own independent search input. The state is shared through Vue's provide/inject system and automatically cleaned up when the scope is disposed.
|
|
4
|
-
|
|
5
|
-
Also exports `provideGlobalSearch()` for framework-level initialization.
|
|
6
|
-
|
|
7
|
-
## When to Use
|
|
8
|
-
|
|
9
|
-
- In blade toolbars or headers to toggle and read the search input visibility for a specific blade
|
|
10
|
-
- To programmatically set or read the search query for a specific blade (e.g., from a URL parameter or external trigger)
|
|
11
|
-
- To integrate search with the blade toolbar's search icon button
|
|
12
|
-
- When NOT to use: for table-level filtering within a blade, use VcDataTable's built-in search header. Global search is for blade-level search that affects the entire blade's content.
|
|
13
|
-
|
|
14
|
-
## Quick Start
|
|
15
|
-
|
|
16
|
-
```vue
|
|
17
|
-
<script setup lang="ts">
|
|
18
|
-
import { useGlobalSearch } from '@vc-shell/framework';
|
|
19
|
-
import { computed, watch } from 'vue';
|
|
20
|
-
|
|
21
|
-
const props = defineProps<{ bladeId: string }>();
|
|
22
|
-
const { isSearchVisible, searchQuery, toggleSearch, setSearchQuery, closeSearch } = useGlobalSearch();
|
|
23
|
-
|
|
24
|
-
// Reactive accessors scoped to this blade
|
|
25
|
-
const isVisible = computed(() => isSearchVisible.value[props.bladeId] ?? false);
|
|
26
|
-
const query = computed(() => searchQuery.value[props.bladeId] ?? '');
|
|
27
|
-
|
|
28
|
-
// React to search query changes
|
|
29
|
-
watch(query, (newQuery) => {
|
|
30
|
-
if (newQuery.length >= 2) {
|
|
31
|
-
fetchResults(newQuery);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
function onSearchToggle() {
|
|
36
|
-
toggleSearch(props.bladeId);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function onSearchClose() {
|
|
40
|
-
closeSearch(props.bladeId);
|
|
41
|
-
}
|
|
42
|
-
</script>
|
|
43
|
-
|
|
44
|
-
<template>
|
|
45
|
-
<div>
|
|
46
|
-
<VcButton icon="fas fa-search" @click="onSearchToggle" />
|
|
47
|
-
<VcInput
|
|
48
|
-
v-if="isVisible"
|
|
49
|
-
:model-value="query"
|
|
50
|
-
placeholder="Search..."
|
|
51
|
-
@update:model-value="(val) => setSearchQuery(bladeId, val)"
|
|
52
|
-
@keydown.escape="onSearchClose"
|
|
53
|
-
/>
|
|
54
|
-
</div>
|
|
55
|
-
</template>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## API
|
|
59
|
-
|
|
60
|
-
### Parameters
|
|
61
|
-
|
|
62
|
-
None.
|
|
63
|
-
|
|
64
|
-
### Returns (`GlobalSearchState`)
|
|
65
|
-
|
|
66
|
-
| Property / Method | Type | Description |
|
|
67
|
-
|-------------------|------|-------------|
|
|
68
|
-
| `isSearchVisible` | `Ref<Record<string, boolean>>` | Map of blade IDs to search visibility state. Access via `isSearchVisible.value[bladeId]`. |
|
|
69
|
-
| `searchQuery` | `Ref<Record<string, string>>` | Map of blade IDs to current search query strings. Access via `searchQuery.value[bladeId]`. |
|
|
70
|
-
| `toggleSearch` | `(bladeId: string) => void` | Toggles search visibility for a blade. If visible, hides it (and clears the query). If hidden, shows it. |
|
|
71
|
-
| `setSearchQuery` | `(bladeId: string, query: string) => void` | Sets the search query for a blade. Does not affect visibility. |
|
|
72
|
-
| `closeSearch` | `(bladeId: string) => void` | Hides the search input for a blade and clears the query. |
|
|
73
|
-
|
|
74
|
-
### Additional Exports
|
|
75
|
-
|
|
76
|
-
| Export | Description |
|
|
77
|
-
|--------|-------------|
|
|
78
|
-
| `provideGlobalSearch()` | Creates and provides the global search service. Idempotent -- returns existing service if already provided. Cleans up all state on scope disposal. |
|
|
79
|
-
|
|
80
|
-
## How It Works
|
|
81
|
-
|
|
82
|
-
The service is a simple reactive state container with two `Ref<Record<string, ...>>` maps. Each blade ID is a key in these maps. This design means:
|
|
83
|
-
|
|
84
|
-
1. **Blades are isolated**: Each blade has its own search visibility and query. Opening search in one blade does not affect others in the stack.
|
|
85
|
-
2. **Lazy initialization**: A blade's entry in the map is created on first `toggleSearch` or `setSearchQuery` call. Reading a non-existent key returns `undefined`, which the consumer treats as `false`/empty.
|
|
86
|
-
3. **Cleanup**: When `provideGlobalSearch()` scope is disposed (e.g., app unmount), both maps are cleared.
|
|
87
|
-
|
|
88
|
-
## Recipe: Toolbar Search Button with Badge Indicator
|
|
89
|
-
|
|
90
|
-
```vue
|
|
91
|
-
<script setup lang="ts">
|
|
92
|
-
import { useGlobalSearch } from '@vc-shell/framework';
|
|
93
|
-
import { computed } from 'vue';
|
|
94
|
-
|
|
95
|
-
const props = defineProps<{ bladeId: string }>();
|
|
96
|
-
const { isSearchVisible, searchQuery, toggleSearch } = useGlobalSearch();
|
|
97
|
-
|
|
98
|
-
const hasActiveSearch = computed(() => {
|
|
99
|
-
const query = searchQuery.value[props.bladeId];
|
|
100
|
-
return query != null && query.length > 0;
|
|
101
|
-
});
|
|
102
|
-
</script>
|
|
103
|
-
|
|
104
|
-
<template>
|
|
105
|
-
<VcButton
|
|
106
|
-
:icon="hasActiveSearch ? 'fas fa-search-plus' : 'fas fa-search'"
|
|
107
|
-
:variant="hasActiveSearch ? 'primary' : 'ghost'"
|
|
108
|
-
@click="toggleSearch(bladeId)"
|
|
109
|
-
/>
|
|
110
|
-
</template>
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Recipe: Pre-Populating Search from URL Query Parameter
|
|
114
|
-
|
|
115
|
-
```vue
|
|
116
|
-
<script setup lang="ts">
|
|
117
|
-
import { useGlobalSearch } from '@vc-shell/framework';
|
|
118
|
-
import { useRoute } from 'vue-router';
|
|
119
|
-
import { onMounted } from 'vue';
|
|
120
|
-
|
|
121
|
-
const props = defineProps<{ bladeId: string }>();
|
|
122
|
-
const route = useRoute();
|
|
123
|
-
const { setSearchQuery, toggleSearch } = useGlobalSearch();
|
|
124
|
-
|
|
125
|
-
onMounted(() => {
|
|
126
|
-
const urlQuery = route.query.search as string | undefined;
|
|
127
|
-
if (urlQuery) {
|
|
128
|
-
setSearchQuery(props.bladeId, urlQuery);
|
|
129
|
-
toggleSearch(props.bladeId); // make the search input visible
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
</script>
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## Tips
|
|
136
|
-
|
|
137
|
-
- **`toggleSearch` clears the query when hiding.** This is by design -- when the user closes the search, the query is reset. If you need to preserve the query across toggle cycles, store it separately.
|
|
138
|
-
- **Use blade ID, not component instance ID.** The blade ID comes from the blade descriptor and is stable across re-renders. Using a component's `uid` would break if the component is recreated.
|
|
139
|
-
- **State is not persisted.** Unlike sidebar state, search state is in-memory only. Refreshing the page clears all search queries. Use URL query parameters if you need persistence.
|
|
140
|
-
- **Calling outside VcApp throws.** Like all provide/inject composables in the framework, `useGlobalSearch()` throws an `InjectionError` if the service has not been provided.
|
|
141
|
-
|
|
142
|
-
## Related
|
|
143
|
-
|
|
144
|
-
- VcDataTable search header -- table-level filtering built into the data table (different from global search)
|
|
145
|
-
- [useToolbar](../useToolbar/) -- toolbar service for blade action buttons (often includes a search toggle button)
|
|
146
|
-
- `framework/core/services/global-search-service.ts` -- underlying service implementation
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# useMenuExpanded
|
|
2
|
-
|
|
3
|
-
Manages the sidebar menu expanded/collapsed and hover-expanded state with localStorage persistence. This is the low-level composable that powers the sidebar pin/unpin behavior in the vc-shell admin UI. It tracks two independent states: the permanent "pinned" state (persisted across sessions) and the transient "hover-expanded" state (active only while the user hovers over a collapsed sidebar).
|
|
4
|
-
|
|
5
|
-
## When to Use
|
|
6
|
-
|
|
7
|
-
- Low-level control over sidebar pin and hover state
|
|
8
|
-
- Building a custom sidebar component that needs expand/collapse persistence
|
|
9
|
-
- When NOT to use: prefer `useSidebarState` which wraps this composable and adds mobile menu support, derived `isExpanded` computed, and responsive breakpoint handling
|
|
10
|
-
|
|
11
|
-
## Basic Usage
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import { useMenuExpanded } from '@vc-shell/framework';
|
|
15
|
-
|
|
16
|
-
const { isExpanded, toggleExpanded, isHoverExpanded, toggleHoverExpanded } = useMenuExpanded();
|
|
17
|
-
|
|
18
|
-
// Toggle pin state (persisted to localStorage)
|
|
19
|
-
toggleExpanded();
|
|
20
|
-
|
|
21
|
-
// Hover expand with 200ms delay
|
|
22
|
-
toggleHoverExpanded(true); // opens after delay
|
|
23
|
-
toggleHoverExpanded(false); // closes immediately
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## API
|
|
27
|
-
|
|
28
|
-
### Returns
|
|
29
|
-
|
|
30
|
-
| Property | Type | Description |
|
|
31
|
-
|---|---|---|
|
|
32
|
-
| `isExpanded` | `Ref<boolean>` | Pinned state, persisted via `useLocalStorage` |
|
|
33
|
-
| `toggleExpanded` | `() => void` | Toggle the pinned state |
|
|
34
|
-
| `isHoverExpanded` | `Ref<boolean>` | Temporary hover expansion (not persisted) |
|
|
35
|
-
| `toggleHoverExpanded` | `(shouldExpand?: boolean) => void` | Set hover state; opening has a 200ms delay, closing is immediate |
|
|
36
|
-
|
|
37
|
-
## Recipe: Custom Sidebar with Hover Expand
|
|
38
|
-
|
|
39
|
-
A common pattern is binding mouse events on the sidebar rail so the menu previews its full width on hover, without permanently pinning it open:
|
|
40
|
-
|
|
41
|
-
```vue
|
|
42
|
-
<script setup lang="ts">
|
|
43
|
-
import { computed } from "vue";
|
|
44
|
-
import { useMenuExpanded } from "@vc-shell/framework";
|
|
45
|
-
|
|
46
|
-
const { isExpanded, toggleExpanded, isHoverExpanded, toggleHoverExpanded } = useMenuExpanded();
|
|
47
|
-
|
|
48
|
-
// The sidebar is visually expanded if pinned OR hover-expanded
|
|
49
|
-
const isVisuallyExpanded = computed(() => isExpanded.value || isHoverExpanded.value);
|
|
50
|
-
</script>
|
|
51
|
-
|
|
52
|
-
<template>
|
|
53
|
-
<aside
|
|
54
|
-
:class="{ 'sidebar--expanded': isVisuallyExpanded }"
|
|
55
|
-
@mouseenter="toggleHoverExpanded(true)"
|
|
56
|
-
@mouseleave="toggleHoverExpanded(false)"
|
|
57
|
-
>
|
|
58
|
-
<nav>
|
|
59
|
-
<!-- menu items -->
|
|
60
|
-
</nav>
|
|
61
|
-
<button @click="toggleExpanded">
|
|
62
|
-
{{ isExpanded ? 'Unpin' : 'Pin' }}
|
|
63
|
-
</button>
|
|
64
|
-
</aside>
|
|
65
|
-
</template>
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Details
|
|
69
|
-
|
|
70
|
-
- **Storage key scoping**: The key is scoped per application using the first URL path segment: `VC_APP_MENU_EXPANDED_{appName}`. For example, if the app is hosted at `/vendor-portal/`, the key is `VC_APP_MENU_EXPANDED_vendor-portal`. This allows multiple vc-shell apps on the same domain to maintain independent sidebar states.
|
|
71
|
-
- **Hover delay**: Opening uses a 200ms debounce to prevent accidental expansion when the cursor briefly passes over the sidebar. Closing is immediate to feel responsive.
|
|
72
|
-
- **Cleanup**: Pending hover timeouts are cleaned up via `onScopeDispose` to prevent memory leaks when the composable's effect scope is destroyed.
|
|
73
|
-
- **Default state**: The sidebar starts pinned open (`true`) on first visit, which is the most user-friendly default for new users.
|
|
74
|
-
|
|
75
|
-
## Tips
|
|
76
|
-
|
|
77
|
-
- If you call `toggleHoverExpanded(true)` and then `toggleHoverExpanded(false)` within the 200ms window, the expansion is canceled -- the timeout is cleared before it fires.
|
|
78
|
-
- The composable does not handle mobile breakpoints. For responsive behavior, use `useSidebarState` instead, which combines `useMenuExpanded` with mobile detection.
|
|
79
|
-
- Each call to `useMenuExpanded()` creates a new instance with its own timeout tracking, but they share the same localStorage-backed `isExpanded` ref (via `useLocalStorage`). Multiple instances will stay in sync for the pinned state.
|
|
80
|
-
|
|
81
|
-
## Related
|
|
82
|
-
|
|
83
|
-
- `useSidebarState` -- higher-level composable that adds mobile menu and derived `isExpanded` computed
|