@vc-shell/vc-app-skill 2.5.0 → 2.6.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/VERSION +1 -1
- package/runtime/knowledge/docs/_BUILD_HASH.md +1 -1
- package/runtime/knowledge/docs/core/blade-navigation/blade-nav-composables.docs.md +28 -1
- package/runtime/knowledge/docs/core/composables/useBlade/useBlade.docs.md +1 -1
- package/runtime/knowledge/docs/core/composables/useLatestRequest/useLatestRequest.docs.md +19 -17
- package/runtime/knowledge/docs/core/composables/useModificationTracker/useModificationTracker.docs.md +11 -0
- package/runtime/knowledge/docs/core/composables/usePopup/usePopup.docs.md +2 -0
- package/runtime/knowledge/docs/core/composables/useSidebarState/useSidebarState.docs.md +15 -4
- package/runtime/knowledge/docs/core/composables/useToolbar/useToolbar.docs.md +4 -0
- package/runtime/knowledge/docs/core/services/services.docs.md +0 -1
- package/runtime/knowledge/docs/ui/components/atoms/vc-button/vc-button.docs.md +1 -0
- package/runtime/knowledge/docs/ui/components/molecules/vc-popover/vc-popover.docs.md +3 -2
- package/runtime/knowledge/docs/ui/components/organisms/vc-app/vc-app.docs.md +1 -1
- package/runtime/knowledge/docs/ui/components/organisms/vc-data-table/vc-data-table.docs.md +77 -0
- package/runtime/knowledge/docs/ui/components/organisms/vc-scheduler/vc-scheduler.docs.md +4 -2
- package/runtime/vc-app.md +4 -2
- 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.
|
|
3
|
+
"version": "2.6.0-rc.0",
|
|
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.
|
|
1
|
+
2.6.0-rc.0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Synced from framework at commit
|
|
1
|
+
Synced from framework at commit 9cf874d09 on 2026-09-02T10:19:47.488Z
|
|
@@ -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,
|
|
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
|
|
|
@@ -35,35 +35,37 @@ const search = useLatestRequest();
|
|
|
35
35
|
const items = ref([]);
|
|
36
36
|
|
|
37
37
|
async function load(criteria) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!request.isCurrent()) return; // a newer search already won
|
|
42
|
-
items.value = result;
|
|
43
|
-
} finally {
|
|
44
|
-
request.complete();
|
|
45
|
-
}
|
|
38
|
+
const result = await search.latest(client.search(criteria));
|
|
39
|
+
if (!result) return; // a newer search already won
|
|
40
|
+
items.value = result;
|
|
46
41
|
}
|
|
47
42
|
```
|
|
48
43
|
|
|
49
|
-
|
|
44
|
+
`latest()` owns the bookkeeping: it marks the request as the newest, releases it
|
|
45
|
+
when it settles — including on a throw — and hands back `undefined` if a newer
|
|
46
|
+
request started meanwhile. There is no `finally` to forget.
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
!!! note "When `undefined` is ambiguous"
|
|
49
|
+
`undefined` means superseded. A request whose own successful result can be
|
|
50
|
+
`undefined` cannot distinguish the two, and should use `begin()` below.
|
|
53
51
|
|
|
54
52
|
## API Reference
|
|
55
53
|
|
|
56
54
|
### Returns
|
|
57
55
|
|
|
58
|
-
| Member | Type
|
|
59
|
-
| ------------ |
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
56
|
+
| Member | Type | Description |
|
|
57
|
+
| ------------ | ----------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
58
|
+
| `latest` | `<T>(request: Promise<T>) => Promise<T \| undefined>` | Runs a request, resolving to `undefined` if a newer one superseded it |
|
|
59
|
+
| `begin` | `() => LatestRequest` | Starts a request and supersedes any earlier one. The manual form behind `latest` |
|
|
60
|
+
| `invalidate` | `() => void` | Supersedes the in-flight request without starting a new one |
|
|
61
|
+
| `dispose` | `() => void` | Permanently supersedes everything. Runs automatically when the owning scope stops |
|
|
62
|
+
| `pending` | `Readonly<Ref<boolean>>` | `true` while the newest request is still running |
|
|
64
63
|
|
|
65
64
|
### `LatestRequest`
|
|
66
65
|
|
|
66
|
+
Returned by `begin()`, for the cases `latest()` cannot serve: the request and the
|
|
67
|
+
check living in different functions, or a result that is legitimately `undefined`.
|
|
68
|
+
|
|
67
69
|
| Member | Type | Description |
|
|
68
70
|
| ----------- | --------------- | ---------------------------------------------------------------------------------- |
|
|
69
71
|
| `isCurrent` | `() => boolean` | `false` once a newer request started, or after `invalidate()` / `dispose()` |
|
|
@@ -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
|
|
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()`
|
|
@@ -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
|
|
@@ -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()`
|
|
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
|
|
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 -->
|
|
@@ -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** --
|
|
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
|
|
@@ -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
|
-
-
|
|
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.
|
|
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).
|
package/runtime/vc-app.md
CHANGED
|
@@ -101,6 +101,7 @@ Collect project parameters through dialog, then run CLI in **non-interactive mod
|
|
|
101
101
|
- `--dashboard` — Include a dashboard page
|
|
102
102
|
- `--tenant-routes` — Include tenant-aware routing
|
|
103
103
|
- `--ai-agent` — Include AI agent integration scaffold
|
|
104
|
+
- `--module-federation` — Include the Module Federation host runtime (load remote modules)
|
|
104
105
|
- `--mocks` — Include sample modules with mock data (demo content to explore the framework)
|
|
105
106
|
|
|
106
107
|
### Execution
|
|
@@ -110,7 +111,7 @@ Collect project parameters through dialog, then run CLI in **non-interactive mod
|
|
|
110
111
|
```bash
|
|
111
112
|
npx @vc-shell/create-vc-app@alpha <projectName> \
|
|
112
113
|
--type standalone \
|
|
113
|
-
[--dashboard] [--tenant-routes] [--ai-agent] [--mocks]
|
|
114
|
+
[--dashboard] [--tenant-routes] [--ai-agent] [--module-federation] [--mocks]
|
|
114
115
|
```
|
|
115
116
|
|
|
116
117
|
**IMPORTANT:** Do NOT pass `--module-name` — the starter module is opt-in and unnecessary because this skill generates custom modules via `/vc-app generate`.
|
|
@@ -125,6 +126,7 @@ Available CLI flags (see `cli/create-vc-app/README.md` for full list):
|
|
|
125
126
|
| `--dashboard` | Include Dashboard |
|
|
126
127
|
| `--tenant-routes` | Include tenant routing |
|
|
127
128
|
| `--ai-agent` | Include AI Agent config |
|
|
129
|
+
| `--module-federation` | Include Module Federation host runtime |
|
|
128
130
|
| `--mocks` | Include sample modules with mock data (demo content) |
|
|
129
131
|
| `--overwrite` | Overwrite existing files |
|
|
130
132
|
|
|
@@ -1198,7 +1200,7 @@ After the user confirms the plan:
|
|
|
1198
1200
|
If `DESIGN_PLAN.needsScaffold` is true, run the `/vc-app create` flow:
|
|
1199
1201
|
|
|
1200
1202
|
- Use `DESIGN_PLAN.appName` as the project name
|
|
1201
|
-
- Enable default options (no dashboard, no tenant-routes, no ai-agent, no mocks)
|
|
1203
|
+
- Enable default options (no dashboard, no tenant-routes, no ai-agent, no module-federation, no mocks)
|
|
1202
1204
|
- After scaffold completes, `cd` into the new project directory
|
|
1203
1205
|
- Update project root for subsequent steps
|
|
1204
1206
|
|
|
@@ -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
|