@vc-shell/vc-app-skill 2.6.0 → 2.6.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vc-shell/vc-app-skill",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
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.6.0
1
+ 2.6.1
@@ -1 +1 @@
1
- Synced from framework at commit 1919c06af on 2026-09-22T14:57:56.467Z
1
+ Synced from framework at commit ce2647528 on 2026-09-29T10:57:40.943Z
@@ -102,12 +102,12 @@ const { parts } = formatShortcut(hotkey.mod.s);
102
102
  // isMac === false → parts = ["Ctrl", "S"] → tooltip renders "Ctrl+S" (with separator)
103
103
  ```
104
104
 
105
- The toolbar button itself (`ToolbarBaseButton`) already does this for you: it calls `formatShortcut` internally and renders the result through the `ShortcutKbd` chip component inside a tooltip. `isMac` also determines `matchesEvent`'s runtime resolution of `mod` -- the same flag drives both what the user sees and what actually fires.
105
+ The toolbar button itself (`ToolbarBaseButton`) already does this for you: it calls `formatShortcut` internally and renders the result through the internal `ShortcutTooltip` (`ShortcutKbd` chips inside a `VcTooltip`). The blade and AI panel header controls use the same component. `isMac` also determines `matchesEvent`'s runtime resolution of `mod` -- the same flag drives both what the user sees and what actually fires.
106
106
 
107
107
  ## Accessibility
108
108
 
109
109
  - **`aria-keyshortcuts`.** Set automatically on the button element whenever `shortcut` is present, using the canonical `aria` string from `formatShortcut` (for example `aria-keyshortcuts="Meta+S"`). No shortcut, no attribute -- it's never rendered empty.
110
- - **Tooltip discovery.** A sighted user discovers the shortcut through the `<kbd>` chips shown on hover/focus of the toolbar button (`ShortcutKbd`, wrapped in `VcTooltip`). This is the only visual affordance; there is no separate "shortcuts list" surface in v1.
110
+ - **Tooltip discovery.** A sighted user discovers the shortcut through the `<kbd>` chips shown on hover/focus of the toolbar button, after a 500ms delay (`ShortcutTooltip`). This is the only visual affordance; there is no separate "shortcuts list" surface in v1.
111
111
  - **No button, no discovery.** Both `aria-keyshortcuts` and the tooltip are properties of the rendered toolbar button -- they exist only because `shortcut` lives on an `IBladeToolbar` item that renders as a button. There is currently no way to advertise a shortcut that isn't attached to a visible button (see [Common mistakes](#common-mistakes)).
112
112
  - **Accessible name still comes from the title.** `shortcut` does not change how the button's accessible name is computed -- it still comes from the visible `title` text. Keep `title` non-empty; `shortcut` is additive, not a substitute for a labeled button.
113
113
  - **Suppressed behind a modal.** Blade shortcuts (toolbar and built-in) do not fire while a modal (`aria-modal="true"`) is open, so a shortcut never acts on a blade hidden behind it.
@@ -50,12 +50,12 @@ None. The composable reads breakpoint state from the framework's provide/inject
50
50
  | Property | Type | Default | Description |
51
51
  | ----------- | -------------- | ------- | --------------------------------------------------------------------- |
52
52
  | `isMobile` | `Ref<boolean>` | `false` | `true` when viewport width < 1024px |
53
- | `isDesktop` | `Ref<boolean>` | `true` | `true` when viewport width > 1024px |
53
+ | `isDesktop` | `Ref<boolean>` | `true` | `true` when viewport width >= 1024px |
54
54
  | `isPhone` | `Ref<boolean>` | `false` | `true` when viewport width < 480px |
55
55
  | `isTablet` | `Ref<boolean>` | `false` | `true` when 480px <= viewport width < 1024px |
56
56
  | `isTouch` | `boolean` | `false` | `true` on touch-capable devices (not reactive — set once at app init) |
57
57
 
58
- Breakpoint thresholds: phone < 480px, tablet 480–1023px, desktop > 1024px (bound to `bp.greater("desktop")`, strictly greater than 1024px). These match the framework's `setupBreakpoints()` configuration.
58
+ Breakpoint thresholds: phone < 480px, tablet 480–1023px, desktop >= 1024px (bound to `bp.greaterOrEqual("desktop")`, so exactly 1024px is desktop). These match the framework's `setupBreakpoints()` configuration.
59
59
 
60
60
  Note: `isMobile` is the union of `isPhone` and `isTablet` — it covers all viewports below the desktop threshold.
61
61
 
@@ -0,0 +1,48 @@
1
+ ---
2
+ title: Focus targets
3
+ category: composables
4
+ group: utilities
5
+ internal: true
6
+ ---
7
+
8
+ # Focus targets
9
+
10
+ Where focus goes after a context change, and which code puts it there. A transition that leaves focus on `<body>` makes the next Tab restart at the top of the document, which fails WCAG 2.4.3 Focus Order — so every transition below has a named target, and none of them may end on `<body>`.
11
+
12
+ Two kinds of move appear in the table, and the difference decides what you should expect to see:
13
+
14
+ - **Repair** — `focusIfLoose(getTarget)` moves focus only when nothing meaningful holds it. If the user clicked a row, or the view autofocused a field, focus stays where it is. A repair that declines is correct behaviour, not a miss.
15
+ - **Handoff** — a deliberate `focus()` call, used when the element the user was on stops existing. It moves focus even though something held it, because that something is about to be unmounted.
16
+
17
+ ## Targets per transition
18
+
19
+ | Transition | Target | Kind | Implemented in |
20
+ | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
21
+ | Sign-in, and any route change | the shell workspace (`.vc-app__workspace`, `tabindex="-1"`) | repair | `vc-app.vue` — `watch(route.fullPath)`, plus a `watch(workspaceRef)` because sign-in is not a route change this component can observe |
22
+ | Sign-out | the unauthenticated layout root | repair | `vc-auth-layout.vue` — `onMounted` |
23
+ | Blade open | the blade root | repair | `vc-blade.vue` — `onMounted` |
24
+ | Maximize / Restore — header button | the replacement control (`Restore` after maximize, `Maximize` after restore) | handoff | `shared/header-controls/header-controls.vue` (blade header and AI panel header) — `watch(maximized)`, `flush: "pre"` |
25
+ | Maximize / Restore — `mod+\` shortcut | same as the header button | handoff | same watch. The shortcut dispatches outside the click handlers, so the handoff hangs off the state change rather than either entry point (VCST-5812) |
26
+ | Maximize while focus is outside the blade (sidebar, app bar, global search) | the blade root | repair | `vc-blade.vue` — `watch(renderingState.maximized)`. Maximizing inerts those regions, and an inert node loses focus a tick later, so `focusIsLoose` treats an element inside `[inert]` as loose (VCST-5859) |
27
+ | Save, and any load that raises `loading` | the blade root | repair | `vc-blade.vue` — `watch(props.loading)`. The toolbar re-renders and the control the user activated can stop existing; before this, only a blade that happened to remount recovered, through the mount repair (VCST-5670) |
28
+ | First load, before content has ever rendered | the blade root | park | `vc-blade.vue` — `watch(showSkeleton)`, which parks focus that was inside the blade before the skeleton replaces its controls |
29
+ | Popup close | the opener, or the workspace when the opener is gone | repair | `usePopup` — `focusIfLoose(() => opener ?? focusFallbackTarget())` |
30
+
31
+ A blade mounting inside the workspace requests focus after the workspace does, so the more specific context wins. That ordering is deliberate.
32
+
33
+ ## Asserting it
34
+
35
+ Focus transitions are invisible to axe, so the story-level a11y gate proves nothing here. Perform the action, then read `document.activeElement`:
36
+
37
+ ```js
38
+ document.activeElement.tagName; // must never be "BODY"
39
+ ```
40
+
41
+ Pressing Tab once and checking that focus continues is a weaker check and on some paths no check at all: when the nav precedes `main` in DOM order, a genuine continuation and a restart from the document's first stop land on the same control. The absent `[active]` node is the evidence, not the next stop.
42
+
43
+ Chrome keeps the sequential-focus navigation starting point at a removed node, so Tab can still reach the right control even after focus was lost. That is engine-specific and does nothing for a screen reader — `document.activeElement` is still `BODY` and the focus ring is gone.
44
+
45
+ ## Related
46
+
47
+ - `focusIfLoose`, `focusFallbackTarget` — `framework/core/utilities/focus.ts`
48
+ - VCST-5670 — the ticket this table was written for