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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,178 @@
1
+ # useResponsive
2
+
3
+ Reactive breakpoint state for building responsive blade UIs. Returns refs that indicate the current viewport category (phone, tablet, mobile, desktop) and whether the device supports touch input. Replaces the legacy `$isMobile.value` global properties in templates and `inject(IsMobileKey)` in script setup with a single, consistent API.
4
+
5
+ ## When to Use
6
+
7
+ - Conditionally render desktop vs. mobile layouts in blade pages and components
8
+ - Apply responsive CSS classes based on viewport size
9
+ - Toggle behavior (e.g., disable drag-and-drop on touch devices, switch column visibility)
10
+ - When NOT to use: for CSS-only responsive changes, prefer Tailwind breakpoint prefixes (`md:`, `lg:`) instead of JavaScript-driven conditionals
11
+
12
+ ## Quick Start
13
+
14
+ ```vue
15
+ <script setup lang="ts">
16
+ import { useResponsive } from "@vc-shell/framework";
17
+
18
+ const { isMobile, isDesktop } = useResponsive();
19
+ </script>
20
+
21
+ <template>
22
+ <VcBlade title="Orders">
23
+ <!-- Vue auto-unwraps refs from script setup — no .value needed -->
24
+ <div v-if="isDesktop" class="tw-flex tw-gap-4">
25
+ <OrdersTable />
26
+ <OrdersSummary />
27
+ </div>
28
+ <OrdersTable v-else />
29
+ </VcBlade>
30
+ </template>
31
+ ```
32
+
33
+ ## API
34
+
35
+ ### Parameters
36
+
37
+ None. The composable reads breakpoint state from the framework's provide/inject context, which is set up automatically by `VcApp`.
38
+
39
+ ### Returns (`UseResponsiveReturn`)
40
+
41
+ | Property | Type | Default | Description |
42
+ |---|---|---|---|
43
+ | `isMobile` | `Ref<boolean>` | `false` | `true` when viewport width < 1024px |
44
+ | `isDesktop` | `Ref<boolean>` | `true` | `true` when viewport width >= 1024px |
45
+ | `isPhone` | `Ref<boolean>` | `false` | `true` when viewport width < 480px |
46
+ | `isTablet` | `Ref<boolean>` | `false` | `true` when 480px <= viewport width < 1024px |
47
+ | `isTouch` | `boolean` | `false` | `true` on touch-capable devices (not reactive — set once at app init) |
48
+
49
+ Breakpoint thresholds: phone < 480px, tablet 480–1023px, desktop >= 1024px. These match the framework's `setupBreakpoints()` configuration.
50
+
51
+ Note: `isMobile` is the union of `isPhone` and `isTablet` — it covers all viewports below the desktop threshold.
52
+
53
+ ## How It Works
54
+
55
+ Under the hood, `useResponsive()` calls `inject()` for each breakpoint key (`IsMobileKey`, `IsDesktopKey`, etc.) with sensible defaults. The framework's root `VcApp` component provides these refs during app initialization using VueUse's `useBreakpoints`. Because the return values are the same `Ref<boolean>` instances provided at the app level, they are reactive and shared across all components.
56
+
57
+ The defaults ensure the composable works even outside the VcApp provider tree (e.g., in unit tests or Storybook), defaulting to desktop mode.
58
+
59
+ ## Recipe: Responsive Blade with Mobile Card Layout
60
+
61
+ ```vue
62
+ <script setup lang="ts">
63
+ import { useResponsive, useBlade } from "@vc-shell/framework";
64
+
65
+ const { isMobile, isDesktop } = useResponsive();
66
+ const { openBlade } = useBlade();
67
+ </script>
68
+
69
+ <template>
70
+ <VcBlade title="Products">
71
+ <VcDataTable
72
+ :items="products"
73
+ :total-count="totalCount"
74
+ @row-click="({ data }) => openBlade({ name: 'ProductDetails', param: data.id })"
75
+ >
76
+ <VcColumn
77
+ id="image"
78
+ title=""
79
+ type="image"
80
+ :width="60"
81
+ :always-visible="true"
82
+ mobile-role="image"
83
+ />
84
+ <VcColumn
85
+ id="name"
86
+ :title="t('NAME')"
87
+ :sortable="true"
88
+ :always-visible="true"
89
+ mobile-role="title"
90
+ />
91
+ <!-- Extra columns visible only on desktop -->
92
+ <VcColumn id="sku" :title="t('SKU')" :sortable="true" />
93
+ <VcColumn id="price" :title="t('PRICE')" type="money" :sortable="true" />
94
+ <VcColumn id="createdDate" :title="t('DATE')" type="date-ago" :sortable="true" />
95
+ </VcDataTable>
96
+ </VcBlade>
97
+ </template>
98
+ ```
99
+
100
+ ## Recipe: Touch-Aware Drag and Drop
101
+
102
+ ```vue
103
+ <script setup lang="ts">
104
+ import { useResponsive } from "@vc-shell/framework";
105
+
106
+ const { isTouch } = useResponsive();
107
+ </script>
108
+
109
+ <template>
110
+ <VcDataTable
111
+ :items="items"
112
+ :reorderable-rows="!isTouch"
113
+ @reorder="onReorder"
114
+ >
115
+ <!-- columns -->
116
+ </VcDataTable>
117
+ </template>
118
+ ```
119
+
120
+ ## Common Mistakes
121
+
122
+ **Wrong: using `$isMobile.value` in template (deprecated)**
123
+ ```vue
124
+ <template>
125
+ <!-- $isMobile is a global property — requires .value, no auto-unwrap -->
126
+ <div v-if="$isMobile.value">Mobile</div>
127
+ </template>
128
+ ```
129
+
130
+ **Correct: using `useResponsive()` destructure**
131
+ ```vue
132
+ <script setup lang="ts">
133
+ import { useResponsive } from "@vc-shell/framework";
134
+ const { isMobile } = useResponsive();
135
+ </script>
136
+ <template>
137
+ <!-- Vue auto-unwraps refs from script setup — clean and type-safe -->
138
+ <div v-if="isMobile">Mobile</div>
139
+ </template>
140
+ ```
141
+
142
+ ---
143
+
144
+ **Wrong: using `inject(IsMobileKey)` directly**
145
+ ```vue
146
+ <script setup lang="ts">
147
+ import { inject, ref } from "vue";
148
+ import { IsMobileKey } from "@vc-shell/framework";
149
+ // Verbose, requires importing both inject and the key
150
+ const isMobile = inject(IsMobileKey, ref(false));
151
+ </script>
152
+ ```
153
+
154
+ **Correct: using `useResponsive()`**
155
+ ```vue
156
+ <script setup lang="ts">
157
+ import { useResponsive } from "@vc-shell/framework";
158
+ // One import, one line, all breakpoints available
159
+ const { isMobile } = useResponsive();
160
+ </script>
161
+ ```
162
+
163
+ ## Tips
164
+
165
+ - **Destructure only what you need.** `const { isMobile } = useResponsive()` is fine — unused refs have no overhead since they already exist at the app level.
166
+ - **Prefer CSS breakpoints for styling.** Use `useResponsive()` for structural changes (different component trees), but for spacing/sizing tweaks use Tailwind responsive prefixes (`md:tw-px-4`).
167
+ - **`isTouch` is not reactive.** It's determined once at app startup based on `ontouchstart` / `maxTouchPoints`. It won't change if a user connects a mouse to a tablet mid-session.
168
+ - **Works in tests without providers.** Defaults to desktop mode (`isDesktop: true`, `isMobile: false`), so most component tests don't need to provide breakpoint keys unless testing mobile-specific behavior.
169
+
170
+ ## Migration
171
+
172
+ See [migration guide #36](../../../../migration/36-use-responsive.md) for automated codemod and manual migration instructions.
173
+
174
+ ## Related
175
+
176
+ - `VcApp` — provides the breakpoint refs that `useResponsive()` reads
177
+ - `VcBlade` — uses `isMobile` internally for mobile blade layout
178
+ - `VcDataTable` — uses `isMobile` for mobile card rendering and pull-to-refresh
@@ -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
 
@@ -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,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