@vc-shell/vc-app-skill 2.5.0-pr354.da09837 → 2.6.0-pr370.de147ea

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/runtime/VERSION +1 -1
  3. package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
  4. package/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md +28 -1
  5. package/runtime/knowledge/docs/core/composables/useBlade/useBlade.docs.md +1 -1
  6. package/runtime/knowledge/docs/core/composables/useKeyboardNavigation/useKeyboardNavigation.docs.md +8 -8
  7. package/runtime/knowledge/docs/core/composables/useModificationTracker/useModificationTracker.docs.md +11 -0
  8. package/runtime/knowledge/docs/core/composables/usePopup/usePopup.docs.md +2 -0
  9. package/runtime/knowledge/docs/core/composables/useSidebarState/useSidebarState.docs.md +15 -4
  10. package/runtime/knowledge/docs/core/composables/useSlowNetworkDetection/useSlowNetworkDetection.docs.md +22 -18
  11. package/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md +4 -0
  12. package/runtime/knowledge/docs/core/services/services.docs.md +0 -1
  13. package/runtime/knowledge/docs/ui/components/atoms/vc-button/vc-button.docs.md +1 -0
  14. package/runtime/knowledge/docs/ui/components/molecules/vc-color-input/vc-color-input.docs.md +10 -9
  15. package/runtime/knowledge/docs/ui/components/molecules/vc-date-picker/vc-date-picker.docs.md +21 -20
  16. package/runtime/knowledge/docs/ui/components/molecules/vc-input/vc-input.docs.md +27 -26
  17. package/runtime/knowledge/docs/ui/components/molecules/vc-input-dropdown/vc-input-dropdown.docs.md +1 -0
  18. package/runtime/knowledge/docs/ui/components/molecules/vc-popover/vc-popover.docs.md +3 -2
  19. package/runtime/knowledge/docs/ui/components/molecules/vc-rating/vc-rating.docs.md +16 -11
  20. package/runtime/knowledge/docs/ui/components/molecules/vc-textarea/vc-textarea.docs.md +1 -0
  21. package/runtime/knowledge/docs/ui/components/organisms/vc-app/vc-app.docs.md +1 -1
  22. package/runtime/knowledge/docs/ui/components/organisms/vc-data-table/vc-data-table.docs.md +77 -0
  23. package/runtime/knowledge/docs/ui/components/organisms/vc-dynamic-property/vc-dynamic-property.docs.md +20 -19
  24. package/runtime/knowledge/docs/ui/components/organisms/vc-scheduler/vc-scheduler.docs.md +4 -2
  25. package/runtime/knowledge/docs/core/composables/useMenuExpanded/index.docs.md +0 -93
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vc-shell/vc-app-skill",
3
- "version": "2.5.0-pr354.da09837",
3
+ "version": "2.6.0-pr370.de147ea",
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.5.0
1
+ 2.6.0
@@ -1 +1 @@
1
- Synced from framework at commit 2964ccbe3 on 2026-08-19T10:23:33.758Z
1
+ Synced from framework at commit 1919c06af on 2026-09-22T14:57:56.467Z
@@ -44,10 +44,36 @@ The `createBladeStack` / `createBladeMessaging` factories are internal (not re-e
44
44
 
45
45
  The blade stack state machine. Manages an ordered array of `BladeDescriptor` objects.
46
46
 
47
- ### Factory: `createBladeStack(bladeRegistry, hasAccess?)`
47
+ ### Factory: `createBladeStack(bladeRegistry, hasAccess?, urlSink?)`
48
48
 
49
49
  Creates a new stack instance. Called once by the navigation plugin.
50
50
 
51
+ `urlSink` is where the stack writes the address bar. It defaults to a no-op, so a
52
+ stack created without one (Storybook, standalone mounts, unit tests) simply never
53
+ touches the URL. The plugin passes the router-backed sink.
54
+
55
+ ### The stack owns the URL
56
+
57
+ Every navigation action syncs the URL itself, with the verb the action implies.
58
+ Callers mutate the stack and nothing else — there is no "and now sync the URL"
59
+ step to forget.
60
+
61
+ | Action | Verb | When |
62
+ | --------------------- | --------- | -------------------------------------- |
63
+ | `openWorkspace` | `push` | the resulting active blade has a `url` |
64
+ | `openBlade` | `push` | the resulting active blade has a `url` |
65
+ | `coverCurrentBlade` | `push` | the resulting active blade has a `url` |
66
+ | `replaceCurrentBlade` | `replace` | the resulting active blade has a `url` |
67
+ | `closeBlade` | `replace` | the close was not prevented by a guard |
68
+ | `closeChildren` | `replace` | always |
69
+
70
+ Everything else — `updateBladeQuery`, `setBladeTitle`, `setBladeError`,
71
+ `_restoreStack` — never writes the URL.
72
+
73
+ The router guard restores the stack **from** the URL, so it suppresses the sink
74
+ for the duration of the navigation (`RouterUrlSink.suppressWhile`): during
75
+ resolution the URL is the source, and writing back would re-enter the guard.
76
+
51
77
  ### API
52
78
 
53
79
  | Method | Description |
@@ -162,6 +188,7 @@ The plain data object stored in the stack for each blade:
162
188
  - Close guards return `true` to PREVENT closing (opposite of the legacy convention where `false` prevented closing). The adapter handles the inversion.
163
189
  - `replaceCurrentBlade` destroys the current blade and creates a new one at the same index with the same `parentId`. Use `coverCurrentBlade` to hide instead of destroy — the covering blade's `callParent` reaches the hidden blade's exposed methods.
164
190
  - URL sync only updates the address bar for blades that have a `url` segment. Third-level detail panels without URLs leave the previous URL intact.
191
+ - The stack writes the URL, not the caller. `createUrlSync` remains for the two writes that are not stack mutations: the post-restore reconcile in `VcBladeNavigation` and the debounced table-query-state writer.
165
192
  - `useBladeNavigation()` requires the navigation plugin to be installed. It throws if called before plugin initialization.
166
193
 
167
194
  ## Related
@@ -790,7 +790,7 @@ if (param.value) {
790
790
  1. **Plugin setup:** `BladeNavigationPlugin` (plugin-v2) installs a `BladeStack` and `BladeMessaging` singleton and provides them via Vue's dependency injection.
791
791
  2. **Inside a blade:** `VcBladeSlot` provides a `BladeDescriptor` ref for each blade instance. `useBlade()` injects this descriptor to access identity, register guards, and set errors.
792
792
  3. **Outside a blade:** `useBlade()` falls back to the singleton instances. Only `openBlade` works because it does not need a blade descriptor.
793
- 4. **URL sync:** When blades with a `url` property are opened or closed, `useBlade()` synchronizes the browser URL via `createUrlSync`, enabling deep linking and back/forward navigation.
793
+ 4. **URL sync:** When blades with a `url` property are opened or closed, the blade stack synchronizes the browser URL itself (push on open, replace on close), enabling deep linking and back/forward navigation. `useBlade()` only mutates the stack.
794
794
 
795
795
  <!-- internal:end -->
796
796
 
@@ -71,13 +71,13 @@ function openMenu() {
71
71
 
72
72
  ### Parameters (Options Object)
73
73
 
74
- | Parameter | Type | Default | Description |
75
- | ------------------- | --------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------- |
76
- | `containerSelector` | `string` | `'[role="menu"]'` | CSS selector to auto-find the container on mount. Set to empty string to disable auto-attach. |
77
- | `itemSelector` | `string` | `'[tabindex="0"]'` | CSS selector for focusable items inside the container. |
78
- | `onEnter` | `(el: HTMLElement) => void` | `undefined` | Called when Enter or Space is pressed on a focused item. Falls back to `el.click()` if not provided. |
79
- | `onEscape` | `() => void` | `undefined` | Called when Escape is pressed. Typically used to close the menu or popover. |
80
- | `loop` | `boolean` | `true` | When `true`, pressing ArrowDown on the last item wraps to the first, and ArrowUp on the first wraps to the last. |
74
+ | Parameter | Type | Default | Description |
75
+ | ------------------- | --------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------- |
76
+ | `containerSelector` | `string` | `'[role="menu"]'` | CSS selector to auto-find the container on mount. Set to empty string to disable auto-attach. |
77
+ | `itemSelector` | `string` | `'[tabindex="0"]'` | CSS selector for focusable items inside the container. |
78
+ | `onEnter` | `(el: HTMLElement) => void` | `undefined` | Called when Enter or Space is pressed on a focused item. Falls back to `el.click()` if not provided. |
79
+ | `onEscape` | `() => void` | `undefined` | Called when Escape is pressed, and makes the composable consume the key. Typically used to close the menu or popover. |
80
+ | `loop` | `boolean` | `true` | When `true`, pressing ArrowDown on the last item wraps to the first, and ArrowUp on the first wraps to the last. |
81
81
 
82
82
  ### Returns
83
83
 
@@ -101,7 +101,7 @@ The composable listens for `keydown` events on the container element and handles
101
101
  - **ArrowDown / Tab**: Move focus to the next item (`focusNextElement`)
102
102
  - **ArrowUp / Shift+Tab**: Move focus to the previous item (`focusPreviousElement`)
103
103
  - **Enter / Space**: Activate the focused item (calls `onEnter` callback, or `el.click()`)
104
- - **Escape**: Call `onEscape` callback
104
+ - **Escape**: Call `onEscape` callback. When a callback is given the event is consumed (`preventDefault` + `stopPropagation`), so a sidebar or dialog listening on `document` does not also close behind the overlay. With no `onEscape`, the key passes through untouched.
105
105
 
106
106
  On each key event, the composable re-queries the container for matching items (`itemSelector`), so it naturally handles dynamic lists where items are added or removed. The focused index is validated against the current item count before every operation to prevent stale-index bugs.
107
107
 
@@ -6,6 +6,17 @@ group: forms
6
6
 
7
7
  # useModificationTracker
8
8
 
9
+ !!! warning "Deprecated — use `useBladeForm`"
10
+ `useBladeForm` owns validation, the dirty flag, the unsaved-changes guard and the
11
+ close prompt together, where this tracks only the dirty flag. It has no callers
12
+ left in the framework or the reference app, and `cli/migrate`'s `use-blade-form`
13
+ transform already flags the `useForm` + `useBeforeUnload` +
14
+ `useModificationTracker` combination for the same move.
15
+
16
+ Its comparator is deliberately not being aligned with `useBladeForm`'s: an
17
+ empty array counts as empty there and not here, and changing that would move
18
+ the dirty flag under anyone still on this composable.
19
+
9
20
  Tracks deep changes to a value and reports whether it has been modified compared to its original (pristine) state. This is the core composable behind the "unsaved changes" detection pattern in vc-shell blades. It handles complex nested objects, arrays, and the common API quirk where `null`, `undefined`, and `""` should be treated as equivalent empty values.
10
21
 
11
22
  The composable maintains two copies of the data: the `pristineValue` (the clean baseline) and the `currentValue` (the working copy bound to form fields). Any deep difference between the two sets `isModified` to `true`.
@@ -110,6 +110,8 @@ The plugin instance is resolved via `inject(PopupPluginKey)`. If called outside
110
110
 
111
111
  Multiple popups can be open simultaneously — they stack with the most recent on top.
112
112
 
113
+ On close, focus returns to the control that opened the popup (WCAG 2.4.3 Focus Order). When that control was never focused, or has since been removed from the page, focus goes to the app workspace instead — never left on `<body>`, which would make the next Tab restart at the top of the document. A popup that deliberately moved focus somewhere else keeps it; this repairs lost focus rather than dictating it.
114
+
113
115
  ## Recipe: Delete Confirmation with Notification
114
116
 
115
117
  ```vue
@@ -8,8 +8,6 @@ group: ui-state
8
8
 
9
9
  Controls the sidebar (left navigation panel) expansion, pinning, hover, and mobile menu state. This composable exposes a unified API for reading and toggling all sidebar states from any component within the VcApp tree. The sidebar has three independent dimensions: **pinned** (user explicitly locked it open, persisted to localStorage), **hover-expanded** (mouse is hovering over the collapsed sidebar on desktop), and **mobile menu** (overlay drawer on small screens). The derived `isExpanded` computed combines pinned and hover states for convenience.
10
10
 
11
- Internally, it delegates to `useMenuExpanded()` to share reactive state with other components like `UserDropdownButton` that also need to know the sidebar state.
12
-
13
11
  ## When to Use
14
12
 
15
13
  - Toggle or read sidebar expanded/collapsed state from any component inside VcApp
@@ -82,6 +80,20 @@ provideSidebarState();
82
80
 
83
81
  <!-- internal:end -->
84
82
 
83
+ ## Storage key
84
+
85
+ The pinned state lives in `localStorage` under an app-scoped key -- the first segment of the URL path, so several vc-shell apps on one domain keep independent sidebars. `sidebarStorageKey()` returns that key, so tests and stories can seed the state before `VcApp` mounts instead of hard-coding the string:
86
+
87
+ ```typescript
88
+ import { sidebarStorageKey } from "@vc-shell/framework";
89
+
90
+ // Served from "/operations/catalog" -> "VC_APP_MENU_EXPANDED_operations"
91
+ localStorage.setItem(sidebarStorageKey(), "true");
92
+
93
+ // Or name the app explicitly
94
+ localStorage.setItem(sidebarStorageKey("operations"), "false");
95
+ ```
96
+
85
97
  ## Recipe: Auto-Close Mobile Menu After Navigation
86
98
 
87
99
  ```vue
@@ -138,10 +150,9 @@ const contentClass = computed(() => (isExpanded.value ? "tw-ml-64" : "tw-ml-16")
138
150
  ## Tips
139
151
 
140
152
  - **Hover delay prevents flicker.** The 200ms delay on `setHoverExpanded(true)` means that quickly passing the mouse over the sidebar does not cause it to flash open. Closing is instant so the sidebar feels responsive.
141
- - **`isPinned` is persisted to localStorage.** If a user pins the sidebar, it stays pinned across page reloads and browser sessions. The key is managed internally by `useMenuExpanded`.
153
+ - **`isPinned` is persisted to localStorage.** If a user pins the sidebar, it stays pinned across page reloads and browser sessions. The key is app-scoped -- `sidebarStorageKey()` returns it.
142
154
  - **Do not call outside VcApp.** If you call `useSidebarState()` in a component that is not a descendant of VcApp (e.g., in a standalone dialog or a separate Vue app instance), it will throw an error.
143
155
 
144
156
  ## Related
145
157
 
146
- - `useMenuExpanded` -- lower-level composable that manages pinned/hover state and localStorage persistence
147
158
  - `VcApp` -- the root component that calls `provideSidebarState()`
@@ -7,15 +7,17 @@ internal: true
7
7
 
8
8
  # useSlowNetworkDetection
9
9
 
10
- 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`.
10
+ Detects slow network conditions and publishes them as the `vc-slow-network` class on `<html>`, which the framework renders as a 2px sweep along the top edge of the viewport. 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 idempotent API requests that have been pending for more than 10 seconds. The class is removed with a 3-second delay after conditions clear, preventing flicker. When the browser goes fully offline, the class is dropped in favor of the offline notification from `useConnectionStatus`.
11
+
12
+ A slow network is a state, not an event, so it is never announced as a toast: a dismissable one reappears on every action that outruns the threshold, and a sticky one sits on the screen for the whole session on a 2g connection (VCST-6045).
11
13
 
12
14
  Like `useConnectionStatus`, this is a module-level singleton — calling it from multiple components shares the same state and listeners.
13
15
 
14
16
  ## When to Use
15
17
 
16
18
  - Reactively check `isSlowNetwork` to show skeleton loaders, disable auto-refresh, or display inline warnings
17
- - The notification is automatic — no extra code needed beyond calling the composable once at app startup (already done in `framework/index.ts`)
18
- - Use `trackRequest` / `untrackRequest` only if you have custom fetch logic outside the standard interceptor (the built-in fetch interceptor already tracks all `/api/*` requests)
19
+ - The `vc-slow-network` class is automatic — no extra code needed beyond calling the composable once at app startup (already done in `framework/index.ts`)
20
+ - Use `trackRequest` / `untrackRequest` only if you have custom fetch logic outside the standard interceptor (the built-in fetch interceptor already tracks idempotent `/api/*` requests)
19
21
  - When NOT to use: for offline detection (use `useConnectionStatus` instead) or for measuring exact latency/bandwidth
20
22
 
21
23
  ## Quick Start
@@ -61,11 +63,12 @@ const { isSlowNetwork } = useSlowNetworkDetection();
61
63
 
62
64
  These are module-private `const` declarations, not exported symbols — do not `import` them. They document the thresholds baked into the composable.
63
65
 
64
- | Name | Value | Purpose |
65
- | --------------------------- | ------------------- | --------------------------------------------------- |
66
- | `SLOW_REQUEST_THRESHOLD_MS` | `10000` | Time before a pending request is considered slow |
67
- | `DISMISS_DELAY_MS` | `3000` | Delay before hiding the notification after recovery |
68
- | `SLOW_EFFECTIVE_TYPES` | `["slow-2g", "2g"]` | Connection types flagged as slow |
66
+ | Name | Value | Purpose |
67
+ | --------------------------- | ------------------- | ------------------------------------------------ |
68
+ | `SLOW_REQUEST_THRESHOLD_MS` | `10000` | Time before a pending request is considered slow |
69
+ | `DISMISS_DELAY_MS` | `3000` | Delay before dropping the class after recovery |
70
+ | `SLOW_EFFECTIVE_TYPES` | `["slow-2g", "2g"]` | Connection types flagged as slow |
71
+ | `SLOW_NETWORK_CLASS` | `"vc-slow-network"` | Class the state is published as, on `<html>` |
69
72
 
70
73
  ## How It Works
71
74
 
@@ -75,15 +78,15 @@ On first call, the composable checks `navigator.connection.effectiveType` (Netwo
75
78
 
76
79
  ### Channel 2: Request timers (reactive)
77
80
 
78
- 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.
81
+ The fetch interceptor in `framework/core/interceptors/index.ts` calls `trackRequest(id)` before every idempotent `/api/*` request (GET and HEAD) and `untrackRequest(id)` in the `finally` block. An upload or any other mutation is left out: its duration is the operator's uplink, not a stalled server, and a large file would otherwise claim the network is slow every time. 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.
79
82
 
80
- ### Notification lifecycle
83
+ ### Class lifecycle
81
84
 
82
- 1. `isSlowNetwork` becomes `true` → show `notification.warning` (deduplicated by fixed ID, persists until removed)
83
- 2. `isSlowNetwork` becomes `false` → start a 3-second dismiss timer
84
- 3. If `isSlowNetwork` goes back to `true` within those 3 seconds → cancel dismiss, notification stays
85
- 4. If the browser goes offline → immediately hide the slow notification (the offline notification takes over)
86
- 5. If the browser comes back online and `isSlowNetwork` is still `true` → re-show
85
+ 1. `isSlowNetwork` becomes `true` → add `vc-slow-network` to `<html>`, and the bar appears
86
+ 2. `isSlowNetwork` becomes `false` → start a 3-second removal timer
87
+ 3. If `isSlowNetwork` goes back to `true` within those 3 seconds → cancel removal, the class stays
88
+ 4. If the browser goes offline → remove the class at once (the offline notification takes over)
89
+ 5. If the browser comes back online and `isSlowNetwork` is still `true` → add it again
87
90
  <!-- internal:end -->
88
91
 
89
92
  ## Recipe: Custom Slow-Network Behavior in a Blade
@@ -131,8 +134,9 @@ async function fetchFromExternalApi(url: string) {
131
134
  ## Tips
132
135
 
133
136
  - **Singleton by design.** Multiple calls to `useSlowNetworkDetection()` share the same state. No overhead from calling it in many components.
134
- - **The notification cannot stack.** It uses a fixed `notificationId`, so even if multiple requests become slow simultaneously, only one notification is shown.
135
- - **The 3-second dismiss delay prevents flicker.** Without it, a burst of requests completing one-by-one would cause the notification to flash on and off rapidly.
137
+ - **The state cannot stack.** A class is present or it is not, so concurrent slow requests read as one state.
138
+ - **The 3-second removal delay prevents flicker.** Without it, a burst of requests completing one-by-one would toggle the class on and off rapidly.
139
+ - **The bar lives in one CSS rule.** `framework/assets/styles/index.scss` draws it next to the `html.vc-offline` rule, so an app can restyle or suppress it without touching the composable. Under `prefers-reduced-motion` it becomes a static line.
136
140
  - **`navigator.connection` has limited support.** Only Chromium-based browsers support it. Firefox and Safari users will only get the request-timer channel, which is still effective.
137
141
  - **The composable does not block requests.** Unlike the offline guard in the interceptor, slow-network detection is purely informational — it never prevents or delays a fetch.
138
142
 
@@ -171,4 +175,4 @@ trackRequest("my-request-2");
171
175
 
172
176
  - [`useConnectionStatus`](../useConnectionStatus/useConnectionStatus.docs.md) — offline detection (binary online/offline)
173
177
  - [`registerInterceptors`](https://github.com/VirtoCommerce/vc-shell/blob/main/framework/core/interceptors/index.ts) — the fetch wrapper that calls `trackRequest`/`untrackRequest`
174
- - `notification` from `@core/notifications/notification` — the notification system used to display the warning
178
+ - `framework/assets/styles/index.scss` — the top-edge sweep this class renders as, alongside the `html.vc-offline` rule
@@ -350,6 +350,10 @@ function helperFunction() {
350
350
  | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
351
351
  | `provideToolbarService()` | Creates and provides the toolbar service via Vue injection. Idempotent -- returns existing service if already provided. Cleans up via `onScopeDispose`. |
352
352
 
353
+ !!! note "When you need it"
354
+
355
+ Inside a shell app the framework install already provides the toolbar service, so `provideToolbarService()` returns the existing instance and you do not need to call it. Use it when bootstrapping an isolated tree (stories, tests) that has not installed the framework, the same way as `provideWidgetService()` and `provideDashboardService()`.
356
+
353
357
  ### IToolbarItem
354
358
 
355
359
  | Property | Type | Required | Description |
@@ -41,7 +41,6 @@ Every service follows the same architecture:
41
41
  | **SettingsMenuService** | `createSettingsMenuService()` | `addSettingsMenuItem()` | `ISettingsMenuItem` |
42
42
  | **AppBarWidgetService** | `createAppBarWidgetService()` | `addAppBarWidget()` | `AppBarWidget` |
43
43
  | **AppBarMobileButtonsService** | `createAppBarMobileButtonsService()` | (direct API) | `AppBarButtonContent` |
44
- | **GlobalSearchService** | `createGlobalSearchService()` | (direct API) | per-blade search state |
45
44
  | **LanguageService** | `createLanguageService()` | (direct API) | locale strings |
46
45
 
47
46
  ## Usage
@@ -315,6 +315,7 @@ All visual properties are customizable through CSS custom properties. Each varia
315
315
  - `aria-pressed` reflects the `selected` prop for toggle buttons
316
316
  - Icon-only buttons **must** include `ariaLabel` for screen reader users
317
317
  - Disabled buttons use `pointer-events: none` and `opacity: 0.5` to prevent interaction
318
+ - A button that becomes disabled **while it holds focus** keeps the focus: it switches to `aria-disabled` instead of the native attribute, since a natively disabled element cannot be focused and focus would fall to `<body>` (WCAG 2.4.3). It stays inert, and the user is still on it when the work finishes. Every other disabled button keeps the native attribute and stays out of the tab order.
318
319
  - The `click` event calls `e.preventDefault()` -- use `type="submit"` for form submissions
319
320
 
320
321
  ## Related Components
@@ -50,15 +50,16 @@ const color = ref<string | null>(null);
50
50
 
51
51
  ## Key Props
52
52
 
53
- | Prop | Type | Default | Description |
54
- | ------------------------ | ---------------------- | ----------- | -------------------------------------------------------- |
55
- | `modelValue` | `string \| null` | `undefined` | Color value via `v-model` (hex string or CSS color name) |
56
- | `label` | `string` | -- | Label text above the field |
57
- | `placeholder` | `string` | -- | Placeholder text |
58
- | `clearable` | `boolean` | `false` | Shows a clear button when a value is present |
59
- | `size` | `"default" \| "small"` | `"default"` | Field height variant |
60
- | `error` / `errorMessage` | `boolean` / `string` | -- | Error styling and validation message |
61
- | `disabled` | `boolean` | `false` | Disables the input and color picker |
53
+ | Prop | Type | Default | Description |
54
+ | ------------------------ | ---------------------- | ----------- | ---------------------------------------------------------------------- |
55
+ | `modelValue` | `string \| null` | `undefined` | Color value via `v-model` (hex string or CSS color name) |
56
+ | `label` | `string` | -- | Label text above the field |
57
+ | `ariaLabel` | `string` | -- | Accessible name for the text input when no visible `label` is provided |
58
+ | `placeholder` | `string` | -- | Placeholder text |
59
+ | `clearable` | `boolean` | `false` | Shows a clear button when a value is present |
60
+ | `size` | `"default" \| "small"` | `"default"` | Field height variant |
61
+ | `error` / `errorMessage` | `boolean` / `string` | -- | Error styling and validation message |
62
+ | `disabled` | `boolean` | `false` | Disables the input and color picker |
62
63
 
63
64
  ## Events
64
65
 
@@ -294,26 +294,27 @@ const date = ref<Date | null>(new Date("2025-01-15"));
294
294
 
295
295
  ## Props
296
296
 
297
- | Prop | Type | Default | Description |
298
- | ------------------- | ---------------------------- | ---------------- | ------------------------------------------------------------ |
299
- | `modelValue` | `Date \| string \| null` | `undefined` | Bound value via `v-model` |
300
- | `type` | `"date" \| "datetime-local"` | `"date"` | Date only or date + time mode |
301
- | `label` | `string` | -- | Label text above the field |
302
- | `placeholder` | `string` | auto (localized) | Placeholder text; auto-generated from locale if not provided |
303
- | `hint` | `string` | -- | Helper text below the field |
304
- | `tooltip` | `string` | -- | Tooltip on the label info icon |
305
- | `clearable` | `boolean` | `false` | Shows a clear button when a date is selected |
306
- | `loading` | `boolean` | `false` | Shows a spinning loader icon |
307
- | `size` | `"default" \| "small"` | `"default"` | Field height variant |
308
- | `required` | `boolean` | `false` | Shows a required indicator |
309
- | `error` | `boolean` | `false` | Enables error styling |
310
- | `errorMessage` | `string` | -- | Error message below the field |
311
- | `disabled` | `boolean` | `false` | Disables the date picker |
312
- | `autofocus` | `boolean` | `false` | Focus the field on mount |
313
- | `name` | `string` | `"Field"` | HTML name attribute |
314
- | `multilanguage` | `boolean` | `false` | Shows language badge on the label |
315
- | `currentLanguage` | `string` | -- | Language code for the badge |
316
- | `datePickerOptions` | `VueDatePickerProps` | -- | Pass-through options to VueDatePicker |
297
+ | Prop | Type | Default | Description |
298
+ | ------------------- | ---------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
299
+ | `modelValue` | `Date \| string \| null` | `undefined` | Bound value via `v-model` |
300
+ | `type` | `"date" \| "datetime-local"` | `"date"` | Date only or date + time mode |
301
+ | `label` | `string` | -- | Label text above the field |
302
+ | `ariaLabel` | `string` | -- | Accessible name for the date input when no visible `label` is provided; applied through the library's `ariaLabels.input` option |
303
+ | `placeholder` | `string` | auto (localized) | Placeholder text; auto-generated from locale if not provided |
304
+ | `hint` | `string` | -- | Helper text below the field |
305
+ | `tooltip` | `string` | -- | Tooltip on the label info icon |
306
+ | `clearable` | `boolean` | `false` | Shows a clear button when a date is selected |
307
+ | `loading` | `boolean` | `false` | Shows a spinning loader icon |
308
+ | `size` | `"default" \| "small"` | `"default"` | Field height variant |
309
+ | `required` | `boolean` | `false` | Shows a required indicator |
310
+ | `error` | `boolean` | `false` | Enables error styling |
311
+ | `errorMessage` | `string` | -- | Error message below the field |
312
+ | `disabled` | `boolean` | `false` | Disables the date picker |
313
+ | `autofocus` | `boolean` | `false` | Focus the field on mount |
314
+ | `name` | `string` | `"Field"` | HTML name attribute |
315
+ | `multilanguage` | `boolean` | `false` | Shows language badge on the label |
316
+ | `currentLanguage` | `string` | -- | Language code for the badge |
317
+ | `datePickerOptions` | `VueDatePickerProps` | -- | Pass-through options to VueDatePicker |
317
318
 
318
319
  ## Events
319
320
 
@@ -671,32 +671,33 @@ VcInput blocks the minus key for both `number` and `integer` types. If you need
671
671
 
672
672
  ### Props
673
673
 
674
- | Prop | Type | Default | Description |
675
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------- |
676
- | `modelValue` | `string \| number \| Date \| null` | `undefined` | Bound value via `v-model` |
677
- | `type` | `"text" \| "password" \| "email" \| "tel" \| "url" \| "time" \| "number" \| "integer" \| "date" \| "datetime-local" \| "color"` | `"text"` | Input type. `date`/`datetime-local` delegate to VcDatePicker; `color` delegates to VcColorInput |
678
- | `label` | `string` | -- | Label text displayed above the field |
679
- | `placeholder` | `string` | -- | Placeholder text inside the field |
680
- | `hint` | `string` | -- | Help text displayed below the field |
681
- | `tooltip` | `string` | -- | Tooltip text shown on hover next to the label |
682
- | `prefix` | `string` | -- | Static text rendered before the input value |
683
- | `suffix` | `string` | -- | Static text rendered after the input value |
684
- | `name` | `string` | `"Field"` | HTML `name` attribute on the native input |
685
- | `clearable` | `boolean` | `false` | Shows a clear (x) button when the field has a value |
686
- | `disabled` | `boolean` | `false` | Disables the input (also inherits from `VcInputGroup` context) |
687
- | `required` | `boolean` | `false` | Adds a red asterisk to the label (visual only, no validation) |
688
- | `loading` | `boolean` | `false` | Shows a spinning loader icon inside the field |
689
- | `autofocus` | `boolean` | `false` | Auto-focuses the input on mount |
690
- | `autocomplete` | `string` | -- | Autofill hint set on the native input (e.g. `"username"`, `"current-password"`) |
691
- | `error` | `boolean` | `false` | Activates error styling (red border and ring) |
692
- | `errorMessage` | `string` | -- | Error text shown below the field; also activates error styling when truthy |
693
- | `debounce` | `string \| number` | -- | Delay in ms before emitting model updates |
694
- | `maxlength` | `string \| number` | `"1024"` | Maximum character length |
695
- | `step` | `string` | `"1"` | Step granularity for number inputs |
696
- | `size` | `"default" \| "small"` | `"default"` | Field height variant (36px / 32px) |
697
- | `multilanguage` | `boolean` | `false` | Shows a language indicator badge on the label |
698
- | `currentLanguage` | `string` | -- | Language code to display in the multilanguage badge |
699
- | `datePickerOptions` | `VueDatePickerProps` | -- | Options forwarded to VcDatePicker (only when `type` is `date` or `datetime-local`) |
674
+ | Prop | Type | Default | Description |
675
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------- |
676
+ | `modelValue` | `string \| number \| Date \| null` | `undefined` | Bound value via `v-model` |
677
+ | `type` | `"text" \| "password" \| "email" \| "tel" \| "url" \| "time" \| "number" \| "integer" \| "date" \| "datetime-local" \| "color"` | `"text"` | Input type. `date`/`datetime-local` delegate to VcDatePicker; `color` delegates to VcColorInput |
678
+ | `label` | `string` | -- | Label text displayed above the field |
679
+ | `placeholder` | `string` | -- | Placeholder text inside the field |
680
+ | `hint` | `string` | -- | Help text displayed below the field |
681
+ | `tooltip` | `string` | -- | Tooltip text shown on hover next to the label |
682
+ | `prefix` | `string` | -- | Static text rendered before the input value |
683
+ | `suffix` | `string` | -- | Static text rendered after the input value |
684
+ | `name` | `string` | `"Field"` | HTML `name` attribute on the native input |
685
+ | `clearable` | `boolean` | `false` | Shows a clear (x) button when the field has a value |
686
+ | `disabled` | `boolean` | `false` | Disables the input (also inherits from `VcInputGroup` context) |
687
+ | `required` | `boolean` | `false` | Adds a red asterisk to the label (visual only, no validation) |
688
+ | `loading` | `boolean` | `false` | Shows a spinning loader icon inside the field |
689
+ | `autofocus` | `boolean` | `false` | Auto-focuses the input on mount |
690
+ | `autocomplete` | `string` | -- | Autofill hint set on the native input (e.g. `"username"`, `"current-password"`) |
691
+ | `ariaLabel` | `string` | -- | Accessible name used when no visible `label` is provided; forwarded to the delegated date picker and color input too |
692
+ | `error` | `boolean` | `false` | Activates error styling (red border and ring) |
693
+ | `errorMessage` | `string` | -- | Error text shown below the field; also activates error styling when truthy |
694
+ | `debounce` | `string \| number` | -- | Delay in ms before emitting model updates |
695
+ | `maxlength` | `string \| number` | `"1024"` | Maximum character length |
696
+ | `step` | `string` | `"1"` | Step granularity for number inputs |
697
+ | `size` | `"default" \| "small"` | `"default"` | Field height variant (36px / 32px) |
698
+ | `multilanguage` | `boolean` | `false` | Shows a language indicator badge on the label |
699
+ | `currentLanguage` | `string` | -- | Language code to display in the multilanguage badge |
700
+ | `datePickerOptions` | `VueDatePickerProps` | -- | Options forwarded to VcDatePicker (only when `type` is `date` or `datetime-local`) |
700
701
 
701
702
  ### Events
702
703
 
@@ -268,6 +268,7 @@ Replace the default dropdown toggle with a custom element using the `button` slo
268
268
  | `name` | `string` | -- | HTML name attribute for the input |
269
269
  | `multilanguage` | `boolean` | `false` | Enable multilanguage editing (forwarded to VcSelect) |
270
270
  | `currentLanguage` | `string` | -- | Active language for multilanguage mode (forwarded to VcSelect) |
271
+ | `ariaLabel` | `string` | -- | Accessible name for the value input when no visible `label` is provided |
271
272
 
272
273
  ## Events
273
274
 
@@ -59,6 +59,7 @@ const open = ref(false);
59
59
  | `contentScrollable` | `boolean` | `true` | Enable scrolling in the content area |
60
60
  | `closeOnClickOutside` | `boolean` | `true` | Close when clicking outside |
61
61
  | `closeOnEscape` | `boolean` | `true` | Close on Escape key |
62
+ | `inert` | `boolean` | `false` | Hide the panel from assistive technology and keyboard focus while something covers it. The panel is teleported, so it does not inherit `inert` from an ancestor — pass it explicitly |
62
63
 
63
64
  ## Events
64
65
 
@@ -143,7 +144,7 @@ The panel's z-index comes from the global `--z-critical-floating-panel` token an
143
144
  - Escape key dismisses the panel (configurable via `closeOnEscape`)
144
145
  - Click-outside detection uses `pointerdown` for reliable z-index handling
145
146
  - Supports nested panels via an internal anchor registry -- child panel clicks do not close parent panels
146
- - Exposes `close()` method via template ref
147
+ - Exposes `close()` and `panelEl` (the teleported panel element) via template ref
147
148
 
148
149
  ## Related Components
149
150
 
@@ -157,7 +158,7 @@ The panel's z-index comes from the global `--z-critical-floating-panel` token an
157
158
  - Click-outside detection uses a `pointerdown` listener on `document` rather than a backdrop overlay. This ensures clicks on high-z-index siblings (sidebar, other teleported panels) are still caught.
158
159
  - `panelAnchorRegistry` (`panel-anchor-registry.ts`) is a `WeakMap<Element, Element | null>` that maps each open panel's DOM element to its anchor element. When `anchorRef` is a floating-ui `VirtualElement`, the registry stores its `contextElement` (the live DOM element backing the virtual reference). This is used to distinguish child panels (whose anchor lives inside this panel) from sibling/parent panels when deciding whether to close on outside click.
159
160
  - Nested VcSelect dropdowns teleported to `<body>` are exempt from click-outside via an ARIA `aria-controls` check: if the clicked `.vc-select__dropdown` has an `id` that matches an `aria-controls` attribute inside this panel, the close is suppressed.
160
- - The panel exposes a `close()` method via `defineExpose` for use with template refs.
161
+ - The panel exposes `close()` and `panelEl` via `defineExpose` for use with template refs. `panelEl` is the teleported panel itself — a call site managing focus needs it to answer "is focus still inside?", and cannot reach it any other way.
161
162
  - `useTeleportTarget` resolves the teleport destination (typically `body`) respecting any custom portal container.
162
163
 
163
164
  <!-- internal:end -->
@@ -234,20 +234,23 @@ Use the `"star-and-text"` or `"text"` variant in table cell slots for a compact
234
234
 
235
235
  ## Props
236
236
 
237
- | Prop | Type | Default | Description |
238
- | ------------- | -------------------------------------- | --------- | ---------------------------------------- |
239
- | `modelValue` | `number` | -- | Current rating value to display |
240
- | `max` | `number` | `5` | Maximum rating value (defines the scale) |
241
- | `variant` | `"stars" \| "star-and-text" \| "text"` | `"stars"` | Display variant |
242
- | `label` | `string` | -- | Field label text above the rating |
243
- | `tooltip` | `string` | -- | Tooltip shown on the label |
244
- | `placeholder` | `string` | -- | Text shown when `modelValue` is falsy |
237
+ | Prop | Type | Default | Description |
238
+ | -------------- | -------------------------------------- | --------- | ----------------------------------------------------------------------- |
239
+ | `modelValue` | `number` | -- | Current rating value to display |
240
+ | `max` | `number` | `5` | Maximum rating value (defines the scale) |
241
+ | `variant` | `"stars" \| "star-and-text" \| "text"` | `"stars"` | Display variant |
242
+ | `label` | `string` | -- | Field label text above the rating |
243
+ | `tooltip` | `string` | -- | Tooltip shown on the label |
244
+ | `placeholder` | `string` | -- | Text shown when `modelValue` is falsy |
245
+ | `error` | `boolean` | -- | Marks the field invalid without a message |
246
+ | `errorMessage` | `string` | -- | Validation message shown below the rating; also marks the field invalid |
245
247
 
246
248
  ## Slots
247
249
 
248
- | Slot | Scope | Description |
249
- | --------- | ----- | -------------------------------------------------------------------------------------- |
250
- | `details` | -- | Additional content below the rating (available in `star-and-text` and `text` variants) |
250
+ | Slot | Scope | Description |
251
+ | --------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------- |
252
+ | `details` | -- | Additional content below the rating (available in `star-and-text` and `text` variants) |
253
+ | `error` | -- | Replaces the default validation message element. Keep the slotted element's `id`, or the control's `aria-describedby` points at nothing |
251
254
 
252
255
  ## CSS Variables
253
256
 
@@ -266,6 +269,8 @@ Use the `"star-and-text"` or `"text"` variant in table cell slots for a compact
266
269
  - When no value is present, the `aria-label` falls back to the placeholder text or "No rating".
267
270
  - The label is rendered via `VcLabel` with optional tooltip, maintaining consistent label semantics.
268
271
  - Star icons are decorative and do not receive focus since the component is display-only.
272
+ - A validation message is rendered below the rating and referenced from the control through `aria-describedby`, so a screen reader is told what is wrong rather than only that something is.
273
+ - Invalid state comes from `useFormField`, so it reflects a surrounding `VcInputGroup` as well as the component's own `error` / `errorMessage`.
269
274
 
270
275
  ## Related Components
271
276
 
@@ -251,6 +251,7 @@ const description = ref<string>("");
251
251
  | `name` | `string` | `"Field"` | Field name used for validation/id generation (not applied as an HTML name attribute on the textarea) |
252
252
  | `multilanguage` | `boolean` | `false` | Shows language badge on the label |
253
253
  | `currentLanguage` | `string` | -- | Language code displayed in the badge |
254
+ | `ariaLabel` | `string` | -- | Accessible name used when no visible `label` is provided |
254
255
 
255
256
  ## Events
256
257
 
@@ -79,7 +79,7 @@ VcApp orchestrates several internal systems:
79
79
  1. **Layout** -- switches between `DesktopLayout` (sidebar) and `MobileLayout` (top bar) based on viewport width. The breakpoint is managed by the framework's responsive detection system.
80
80
  2. **Blade Navigation** -- renders `VcBladeNavigation` for the stacked panel workspace. Blades open to the right and scroll horizontally.
81
81
  3. **Module Loading** -- consumes `DynamicModulesKey` for runtime module registration; shows error notifications on failure. Modules declare routes, menu items, and services.
82
- 4. **Services** -- bootstraps shell services (Menu, Toolbar, Settings, Dashboard, GlobalSearch, etc.) via `useShellBootstrap`. These services are available to all child components through provide/inject.
82
+ 4. **Services** -- provides the shell-scoped services via `useShellBootstrap`: dashboard, app bar mobile buttons, and the AI agent when configured. Menu, toolbar, widget and settings services are provided earlier, by the framework install. All are reachable from child components through provide/inject.
83
83
 
84
84
  <!-- internal:end -->
85
85
 
@@ -211,6 +211,83 @@ For any rendering not covered by built-in types, use the `#body` slot:
211
211
  </VcColumn>
212
212
  ```
213
213
 
214
+ ### Custom Cell Types
215
+
216
+ A `#body` slot covers one-off formatting. When the same rendering should apply
217
+ to a column `type` across every table in the app, register a cell component
218
+ instead. Registration is global and reactive — already-mounted tables pick the
219
+ new component up.
220
+
221
+ ```ts
222
+ // src/cells/register.ts — imported once from your app entry point
223
+ import { useCellRegistry } from "@vc-shell/framework";
224
+ import CellRating from "./CellRating.vue";
225
+
226
+ const { register } = useCellRegistry();
227
+
228
+ register({
229
+ type: "rating",
230
+ component: CellRating,
231
+ config: { editable: false },
232
+ });
233
+ ```
234
+
235
+ ```vue
236
+ <VcColumn id="rating" field="rating" title="Rating" type="rating" />
237
+ ```
238
+
239
+ The cell component receives a `value` prop holding the resolved field value.
240
+ When `config.editable` is `true`, it also receives `editable`, `label`,
241
+ `fieldName`, `fieldId`, `rules` and `rowIndex` while the row is being edited —
242
+ mirror `CellDefault.vue` for that contract.
243
+
244
+ Registering an existing type replaces the built-in for the whole app:
245
+
246
+ ```ts
247
+ register({ type: "money", component: MyMoneyCell, config: { editable: true } });
248
+ ```
249
+
250
+ The 12 built-ins are registered by `registerBuiltinCells()`, which runs once
251
+ when the table's cell renderer module is loaded — that is, as soon as anything
252
+ imports `VcDataTable`. Because ES modules finish evaluating before the module
253
+ that imported them runs, that always happens before your own registration code,
254
+ and `register()` is last-write-wins — so a type you register replaces the
255
+ built-in. `registerBuiltinCells()` itself skips types that are already present,
256
+ which keeps it idempotent and stops it clobbering a registration that did manage
257
+ to land first (for instance when the table is loaded through a dynamic
258
+ `import()`). `BUILTIN_CELL_TYPES` lists the names that are taken.
259
+
260
+ A `type` that resolves to nothing falls back to `CellDefault`, which renders the
261
+ raw value. Custom names are allowed, so a misspelt built-in is not a type error;
262
+ in development builds an unresolved type logs a warning naming the type.
263
+
264
+ #### Cell registry API
265
+
266
+ | Member | Signature | Description |
267
+ | -------------------- | ------------------------------------------------- | -------------------------------------------------------- |
268
+ | `register` | `(registration: CellRegistration) => void` | Register or replace a cell type. |
269
+ | `get` | `(type: string) => CellRegistration \| undefined` | Look up a registration. |
270
+ | `has` | `(type: string) => boolean` | Whether a type is registered. |
271
+ | `getRegisteredTypes` | `() => string[]` | All registered type names. |
272
+ | `unregister` | `(type: string) => boolean` | Remove a type. Returns `false` if it was not registered. |
273
+ | `clear` | `() => void` | Remove every registration, including the built-ins. |
274
+
275
+ Two module-level exports come with it:
276
+
277
+ | Export | Signature | Description |
278
+ | ---------------------- | --------------------- | --------------------------------------------------------------------------------------------------------- |
279
+ | `registerBuiltinCells` | `() => void` | Register the 12 built-in cell types. Idempotent and non-destructive; called for you when the table loads. |
280
+ | `BUILTIN_CELL_TYPES` | `readonly CellType[]` | The names of the 12 built-in types. |
281
+
282
+ `CellRegistration` is `{ type: CellType | string; component: Component; config?: { editable?: boolean; additionalProps?: string[] } }`.
283
+
284
+ `clear()` empties the registry permanently — `registerBuiltinCells()` runs once
285
+ per module load, so it will not repopulate on its own. Call it again yourself if
286
+ you need the built-ins back.
287
+
288
+ `CellType` accepts the 12 built-in names with autocomplete plus any custom
289
+ string. `BuiltinCellType` is the narrow union of just the built-ins.
290
+
214
291
  ---
215
292
 
216
293
  ## Sorting
@@ -46,25 +46,26 @@ The prop names match the `DynamicObjectProperty` shape returned by the platform
46
46
 
47
47
  ## Key Props
48
48
 
49
- | Prop | Type | Default | Description |
50
- | -------------------- | ---------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
- | `property` | `T` | _required_ | Property object with `id` and metadata |
52
- | `modelValue` | `any` | _required_ | Current property value (v-model) |
53
- | `valueType` | `string` | _required_ | Type: ShortText, LongText, Number, Integer, DateTime, Boolean, Measure, Color |
54
- | `name` | `string` | _required_ | Property name for display and field identification |
55
- | `required` | `boolean` | _required_ | Whether the field is required |
56
- | `disabled` | `boolean` | `false` | Disables the input |
57
- | `dictionary` | `boolean` | `false` | Uses VcSelect/VcMultivalue with options |
58
- | `multivalue` | `boolean` | `false` | Supports multiple values |
59
- | `multilanguage` | `boolean` | `false` | Supports localized values |
60
- | `currentLanguage` | `string` | - | Active locale used for localized values |
61
- | `optionsGetter` | `Function` | _required_ | Async loader for dictionary options, called as `(propertyId, keyword?, locale?)` |
62
- | `measurementsGetter` | `Function` | - | Async loader for measurement units |
63
- | `optionsValue` | `string` | `"id"` | Field on each option used as its value (VcSelect mapping) |
64
- | `optionsLabel` | `string` | `"value"` | Inert. The option label is fixed to `"value"` when `multilanguage` is true and `"alias"` otherwise; only `optionsValue` is honored for VcSelect mapping. |
65
- | `displayNames` | `{ name?; languageCode? }[]` | - | Localized display names used to resolve the field label per locale |
66
- | `placeholder` | `string` | - | Placeholder text for the input |
67
- | `rules` | `object` | - | Validation rules: `{ min, max, regex }` |
49
+ | Prop | Type | Default | Description |
50
+ | -------------------- | ---------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `property` | `T` | _required_ | Property object with `id` and metadata |
52
+ | `modelValue` | `any` | _required_ | Current property value (v-model) |
53
+ | `valueType` | `string` | _required_ | Type: ShortText, LongText, Number, Integer, DateTime, Boolean, Measure, Color |
54
+ | `name` | `string` | _required_ | Property name for display and field identification |
55
+ | `required` | `boolean` | _required_ | Whether the field is required |
56
+ | `disabled` | `boolean` | `false` | Disables the input |
57
+ | `dictionary` | `boolean` | `false` | Uses VcSelect/VcMultivalue with options |
58
+ | `multivalue` | `boolean` | `false` | Supports multiple values |
59
+ | `multilanguage` | `boolean` | `false` | Supports localized values |
60
+ | `currentLanguage` | `string` | - | Active locale used for localized values |
61
+ | `optionsGetter` | `Function` | _required_ | Async loader for dictionary options, called as `(propertyId, keyword?, locale?)` |
62
+ | `measurementsGetter` | `Function` | - | Async loader for measurement units |
63
+ | `optionsValue` | `string` | `"id"` | Field on each option used as its value (VcSelect mapping) |
64
+ | `optionsLabel` | `string` | `"value"` | Inert. The option label is fixed to `"value"` when `multilanguage` is true and `"alias"` otherwise; only `optionsValue` is honored for VcSelect mapping. |
65
+ | `displayNames` | `{ name?; languageCode? }[]` | - | Localized display names used to resolve the field label per locale |
66
+ | `placeholder` | `string` | - | Placeholder text for the input |
67
+ | `rules` | `object` | - | Validation rules: `{ min, max, regex }` |
68
+ | `hideLabel` | `boolean` | `false` | Hides the control's own visible label so the consumer can render its own label row (e.g. to place an action next to the property name). Validation and the placeholder fallback are unaffected. The display name is forwarded as the control's accessible name for nine control families: short text, number, integer, long text, boolean, datetime, colour, Measure, and the fallback used for an unrecognized `valueType`. Dictionary and multivalue properties (VcSelect, VcMultivalue) are **not** covered — their focusable triggers carry no accessible name, so those properties still need a visible label. |
68
69
 
69
70
  ::storybook id="data-display-vcdynamicproperty--property-form" height="500"
70
71
 
@@ -162,6 +162,8 @@ With `editable`, `VcScheduler` ships a complete built-in create/edit flow -- no
162
162
 
163
163
  The editor modal additionally exposes all-day toggle, start/end date-time, and color, plus a Delete action in edit mode (commits via `event-delete`). Saving from either surface emits the public `event-create`/`event-update` event -- see [Events](#events) for the exact payloads.
164
164
 
165
+ For an **all-day** event the editor's End field is the last day the event covers, matching what the chip and the quick-info popover announce -- so a one-day event has End equal to Start. The `end` in the emitted payload is still the exclusive midnight boundary the layout works in; the editor converts on save. An End before Start is refused rather than saved.
166
+
165
167
  This entire flow is internal state: no `v-model` or extra event wiring is needed to make it work, beyond `editable` and the `event-create`/`event-update`/`event-delete` handlers you already have for persisting the result.
166
168
 
167
169
  ### `editorMode`: opting out of the built-in UI
@@ -498,8 +500,8 @@ const events = [{ id: "a", title: "Promo", start, end, allDay: true, color: "#a2
498
500
  - The Month grid uses `role="grid"` / `role="row"` / `role="gridcell"` / `role="columnheader"` for the weekday header and day cells, each `gridcell` carrying a full formatted-date `aria-label`.
499
501
  - Each event bar and timed chip is a focusable `role="button"` element (`tabindex="0"`) with an `aria-label` built from its title and formatted start/end dates -- activate with click or `Enter`. For an all-day event the announced end is the **last inclusive day**, not the exclusive `end` you pass: an event with `end` at midnight runs through the previous day, so a one-day event is announced as a single date rather than a two-day range. Timed events are announced with their real times.
500
502
  - The "+N more" overflow control is a `<button>` whose visible text is its accessible name; the date it belongs to comes from the enclosing `gridcell`. Activating it moves focus to the first event in the popover, and closing the popover (Escape, or the close button) returns focus to the "+N more" button. Each event inside the popover is a `<button>` too, so a day's 4th and later events are reachable and operable by keyboard.
501
- - The overflow popover's panel is a `VcPopover`, which supplies a titled header (the formatted date) and Escape-to-close. It does **not** apply `role="dialog"` or trap focus -- `VcPopover` has no focus management of its own, so any popover-based surface that needs focus moved into it does that itself, as the overflow popover above does.
502
- - The editor modal and the This event/All events scope dialog are `VcPopup`, which is a Headless UI `Dialog` -- those do get accessible naming, a focus trap, focus restore and `inert` on the background from the library. The built-in quick-create popover is a `VcPopover` and focuses its title field on open.
503
+ - All three anchored popovers -- "+N more" overflow, quick-info and quick-create -- are `VcPopover` panels carrying `role="dialog"` and an accessible name (the formatted date, the event title, and "New event" respectively). They do **not** trap focus: `VcPopover` has no focus management of its own, so each one moves focus into itself on open and hands it back to whatever opened it on close. Focus is repaired, not dictated -- a user who moves focus out of the panel keeps it there when the panel closes.
504
+ - The editor modal and the This event/All events scope dialog are `VcPopup`, which is a Headless UI `Dialog` -- those do get accessible naming, a focus trap, focus restore and `inert` on the background from the library.
503
505
  - The recurrence editor's weekday toggle buttons expose `aria-pressed` (selected state) and an `aria-label` with the full weekday name, since their visible narrow labels ("S", "M", "T", ...) collide (Sun/Sat, Tue/Thu). The "↻" recurring-occurrence marker is `aria-hidden` -- purely visual, redundant with the event's own accessible name.
504
506
  - The toolbar's prev/next buttons carry an explicit `aria-label` ("Previous" / "Next") since they are icon-only; the today/view-switch buttons show text labels.
505
507
  - Event move/resize (Month view's `editable`) is a pointer-drag interaction with no keyboard equivalent yet; use a form outside VcScheduler for keyboard-only date edits. Timeline view is read-only (click still opens the quick-info popover).
@@ -1,93 +0,0 @@
1
- ---
2
- title: useMenuExpanded
3
- category: composables
4
- group: ui-state
5
- slug: useMenuExpanded
6
- ---
7
-
8
- # useMenuExpanded
9
-
10
- 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).
11
-
12
- ## When to Use
13
-
14
- - Low-level control over sidebar pin and hover state
15
- - Building a custom sidebar component that needs expand/collapse persistence
16
- - When NOT to use: prefer `useSidebarState` which wraps this composable and adds mobile menu support, derived `isExpanded` computed, and responsive breakpoint handling
17
-
18
- ## Basic Usage
19
-
20
- ```typescript
21
- import { useMenuExpanded } from "@vc-shell/framework";
22
-
23
- const { isExpanded, toggleExpanded, isHoverExpanded, toggleHoverExpanded } = useMenuExpanded();
24
-
25
- // Toggle pin state (persisted to localStorage)
26
- toggleExpanded();
27
-
28
- // Hover expand with 200ms delay
29
- toggleHoverExpanded(true); // opens after delay
30
- toggleHoverExpanded(false); // closes immediately
31
- ```
32
-
33
- ## API
34
-
35
- ### Returns
36
-
37
- | Property | Type | Description |
38
- | --------------------- | ---------------------------------- | ---------------------------------------------------------------- |
39
- | `isExpanded` | `Ref<boolean>` | Pinned state, persisted via `useLocalStorage` |
40
- | `toggleExpanded` | `() => void` | Toggle the pinned state |
41
- | `isHoverExpanded` | `Ref<boolean>` | Temporary hover expansion (not persisted) |
42
- | `toggleHoverExpanded` | `(shouldExpand?: boolean) => void` | Set hover state; opening has a 200ms delay, closing is immediate |
43
-
44
- ## Recipe: Custom Sidebar with Hover Expand
45
-
46
- 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:
47
-
48
- ```vue
49
- <script setup lang="ts">
50
- import { computed } from "vue";
51
- import { useMenuExpanded } from "@vc-shell/framework";
52
-
53
- const { isExpanded, toggleExpanded, isHoverExpanded, toggleHoverExpanded } = useMenuExpanded();
54
-
55
- // The sidebar is visually expanded if pinned OR hover-expanded
56
- const isVisuallyExpanded = computed(() => isExpanded.value || isHoverExpanded.value);
57
- </script>
58
-
59
- <template>
60
- <aside
61
- :class="{ 'sidebar--expanded': isVisuallyExpanded }"
62
- @mouseenter="toggleHoverExpanded(true)"
63
- @mouseleave="toggleHoverExpanded(false)"
64
- >
65
- <nav>
66
- <!-- menu items -->
67
- </nav>
68
- <button @click="toggleExpanded">
69
- {{ isExpanded ? "Unpin" : "Pin" }}
70
- </button>
71
- </aside>
72
- </template>
73
- ```
74
-
75
- <!-- internal:start -->
76
-
77
- ## Details
78
-
79
- - **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 `/operations/`, the key is `VC_APP_MENU_EXPANDED_operations`. This allows multiple vc-shell apps on the same domain to maintain independent sidebar states.
80
- - **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.
81
- - **Cleanup**: Pending hover timeouts are cleaned up via `onScopeDispose` to prevent memory leaks when the composable's effect scope is destroyed.
82
- - **Default state**: The sidebar starts pinned open (`true`) on first visit, which is the most user-friendly default for new users.
83
- <!-- internal:end -->
84
-
85
- ## Tips
86
-
87
- - If you call `toggleHoverExpanded(true)` and then `toggleHoverExpanded(false)` within the 200ms window, the expansion is canceled -- the timeout is cleared before it fires.
88
- - The composable does not handle mobile breakpoints. For responsive behavior, use `useSidebarState` instead, which combines `useMenuExpanded` with mobile detection.
89
- - 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.
90
-
91
- ## Related
92
-
93
- - `useSidebarState` -- higher-level composable that adds mobile menu and derived `isExpanded` computed