@vc-shell/vc-app-skill 2.3.0 → 2.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vc-shell/vc-app-skill",
3
- "version": "2.3.0",
3
+ "version": "2.4.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.3.0
1
+ 2.4.0
@@ -1 +1 @@
1
- Synced from framework at commit eb3675e7a on 2026-07-28T09:06:26.940Z
1
+ Synced from framework at commit d4d56d4fa on 2026-08-04T07:36:41.124Z
@@ -6,7 +6,7 @@ group: data
6
6
 
7
7
  # useApiClient
8
8
 
9
- Creates a typed API client instance for communicating with VirtoCommerce platform APIs. The composable accepts a generated client class constructor (extending `AuthApiBase`) and returns an async factory function that produces a client instance. The composable itself is intentionally thin — it constructs the client and returns it. Platform authentication flows through the session cookie that the browser replays on every same-origin API call; the framework's fetch wrapper enforces timeout, offline checks, and 401-redirect on top.
9
+ Creates a typed API client instance for communicating with VirtoCommerce platform APIs. The composable accepts a generated client class constructor (extending `AuthApiBase`) and returns an async factory function that produces a client instance. The composable itself is intentionally thin — it constructs the client and returns it. Platform authentication flows through the session cookie that the browser replays on every same-origin API call; the framework's fetch wrapper enforces timeout, offline checks, and redirect-to-login on top. An expired session is detected from a `401` **or** from a response that was redirected to the login page (some platform configurations answer that way instead of a 401, and `fetch` follows the redirect transparently). A `403` is not treated as expiry — authenticated-but-unauthorized keeps the session.
10
10
 
11
11
  !!! tip "Always call getApiClient inside async functions"
12
12
  `getApiClient` is async. Never call it at the top level of `<script setup>` — call it inside the async function you pass to `useAsync`. Holding one client instance for the lifetime of the component couples your code to a single object across action runs; prefer one `await getApiClient()` per action so the call shape stays uniform with `useAsync`.
@@ -62,14 +62,14 @@ None.
62
62
 
63
63
  ### Returns (`ILanguageService`)
64
64
 
65
- | Property / Method | Type | Description |
66
- | ------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
67
- | `currentLocale` | `ComputedRef<string>` | The currently active locale code (e.g., `"en-US"`, `"de-DE"`). |
68
- | `setLocale` | `(locale: string) => void` | Switches the application locale. This updates `vue-i18n`'s locale and triggers re-rendering of all translated text. |
69
- | `getLocaleByTag` | `(localeTag: string) => string \| undefined` | Resolves a locale tag to its native display name. Regional tags stay distinct (e.g., `"en-US"` → `"American English"`, `"en-GB"` → `"British English"`); plain codes resolve to the base native name (`"fr"` → `"Français"`). Returns `undefined` if the tag is not recognized. |
70
- | `resolveCamelCaseLocale` | `(locale: string) => string` | Normalizes and validates a locale code to a supported hyphenated lowercase locale (e.g., `"enUS"` to `"en-us"`), falling back to `"en"` if the locale is not supported. Delegates to `resolveSupportedLocale`. |
71
- | `getFlag` | `(language: string) => Promise<string>` | Fetches a flag image URL for the given language/locale. Returns a promise because flags may be loaded lazily. |
72
- | `getCountryCode` | `(language: string) => string` | Extracts the lowercase country code from a language tag (e.g., `"en-US"` to `"us"`, `"de-DE"` to `"de"`). Falls back to `"xx"` for unknown languages. |
65
+ | Property / Method | Type | Description |
66
+ | ------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
67
+ | `currentLocale` | `ComputedRef<string>` | The currently active locale code (e.g., `"en-US"`, `"de-DE"`). |
68
+ | `setLocale` | `(locale: string) => void` | Switches the application locale. Updates `vue-i18n`'s locale, re-renders translated text, configures vee-validate messages, persists the choice, and sets `<html lang>` to the resolved locale so assistive technology picks the right pronunciation dictionary (WCAG 3.1.1). Applications do not need to manage `<html lang>` themselves. |
69
+ | `getLocaleByTag` | `(localeTag: string) => string \| undefined` | Resolves a locale tag to its native display name. Regional tags stay distinct (e.g., `"en-US"` → `"American English"`, `"en-GB"` → `"British English"`); plain codes resolve to the base native name (`"fr"` → `"Français"`). Returns `undefined` if the tag is not recognized. |
70
+ | `resolveCamelCaseLocale` | `(locale: string) => string` | Normalizes and validates a locale code to a supported hyphenated lowercase locale (e.g., `"enUS"` to `"en-us"`), falling back to `"en"` if the locale is not supported. Delegates to `resolveSupportedLocale`. |
71
+ | `getFlag` | `(language: string) => Promise<string>` | Fetches a flag image URL for the given language/locale. Returns a promise because flags may be loaded lazily. |
72
+ | `getCountryCode` | `(language: string) => string` | Extracts the lowercase country code from a language tag (e.g., `"en-US"` to `"us"`, `"de-DE"` to `"de"`). Falls back to `"xx"` for unknown languages. |
73
73
 
74
74
  ### Additional Exports
75
75
 
@@ -198,6 +198,17 @@ Do not set `triggerAction="none"` and then rely on `@trigger:click` -- the event
198
198
  <SettingsMenuItem trigger-action="click" @trigger:click="doSomething" />
199
199
  ```
200
200
 
201
+ ## Accessibility
202
+
203
+ - The trigger renders as a native `<button type="button">`, so it is in the tab order and Enter/Space activate it. No `tabindex` or `keydown` wiring is needed.
204
+ - Focus ring on `:focus-visible` only, colored via `--menu-item-focus-ring-color`
205
+ - `disabled` is applied to the button element, not only to the click handler — the control is skipped by keyboard navigation instead of being focusable-but-inert
206
+ - With a submenu, the trigger reports `aria-haspopup="true"` and an `aria-expanded` that tracks the open state (both on desktop popover and mobile inline expansion)
207
+ - Without a submenu neither attribute is rendered, so a plain action item is not announced as a menu
208
+
209
+ !!! warning "A custom `#trigger` slot owns its own semantics"
210
+ When you pass `#trigger`, the wrapper stays a `<div>` — nesting your control inside our button would produce invalid, unusable markup. Render an interactive element (a `<button>` or a link) inside the slot yourself, otherwise that item is not keyboard operable.
211
+
201
212
  ## Related Components
202
213
 
203
214
  - [SettingsMenu](../settings-menu/settings-menu.docs.md) -- parent container
@@ -140,6 +140,23 @@ function resetLayout() {
140
140
  - The 12-column grid means common widget widths are: 3 (quarter), 4 (third), 6 (half), and 12 (full width).
141
141
  - Register widgets during module `install()` before the dashboard component mounts. Late registrations may not be picked up.
142
142
 
143
+ ## Accessibility
144
+
145
+ Widgets can be rearranged without a pointer, which WCAG 2.5.7 Dragging Movements requires:
146
+
147
+ | Key | Action |
148
+ | ------------------- | --------------------------------------------------------------- |
149
+ | `Tab` | Move focus between widgets (each one is in the tab order) |
150
+ | `Enter` / `Space` | Pick the focused widget up, and drop it again |
151
+ | Arrow keys | While picked up, move the widget one grid cell |
152
+ | `Shift` + arrow key | While picked up, resize by one cell (needs `resizable`) |
153
+ | `Escape` | Cancel the move and return the widget to where it was picked up |
154
+
155
+ Every step is announced through the component's `aria-live` region, and the picked-up widget is outlined so the state is visible to sighted keyboard users. Moves are clamped at the grid edges and at the 2×2 minimum widget size, and the layout is persisted when the widget is dropped — the same as after a mouse drag.
156
+
157
+ !!! note "The widget itself is the control"
158
+ There is no separate "move" button. Gridstack only implements pointer dragging, so the widget is focusable and handles the keys directly. If you render your own interactive elements inside a widget, they keep working — the arrow keys only act while the widget has been explicitly picked up.
159
+
143
160
  ## Advanced / Exports
144
161
 
145
162
  Besides the `DraggableDashboard` component, `draggable-dashboard/index.ts` re-exports these symbols through the framework root, for building a custom Gridstack dashboard:
@@ -34,12 +34,12 @@ import { VcVideo } from "@vc-shell/framework";
34
34
 
35
35
  ## Key Props
36
36
 
37
- | Prop | Type | Default | Description |
38
- | ------------------- | -------- | ------- | -------------------------------------------------------------------------------------- |
39
- | `source` | `string` | -- | Embed URL for the video (e.g., YouTube embed link) |
40
- | `label` | `string` | -- | Label text displayed above the video |
41
- | `tooltip` | `string` | -- | Tooltip text shown on the label's info icon |
42
- | `additionalSandbox` | `string` | -- | Extra space-separated iframe sandbox tokens appended to the secure default (see below) |
37
+ | Prop | Type | Default | Description |
38
+ | ------------------- | -------- | ------- | ------------------------------------------------------------------------------------------- |
39
+ | `source` | `string` | -- | Embed URL for the video (e.g., YouTube embed link) |
40
+ | `label` | `string` | -- | Label text displayed above the video |
41
+ | `tooltip` | `string` | -- | Tooltip text shown on the label's info icon |
42
+ | `additionalSandbox` | `string` | -- | Extra space-separated iframe sandbox tokens appended to the resolved sandbox (see Security) |
43
43
 
44
44
  ::storybook id="data-display-vcvideo--with-tooltip" height="400"
45
45
 
@@ -117,7 +117,7 @@ When `source` is not provided, VcVideo renders a centered film icon placeholder
117
117
 
118
118
  - Always use the **embed** URL format, not the standard watch URL. For YouTube, use `https://www.youtube.com/embed/VIDEO_ID` instead of `https://www.youtube.com/watch?v=VIDEO_ID`.
119
119
  - The iframe has `loading="lazy"`, so videos below the fold are not loaded until the user scrolls to them. This keeps initial page load fast.
120
- - The `sandbox` attribute defaults to the secure minimum `allow-scripts allow-presentation`. If your video host requires more, append tokens via `additionalSandbox` — see the Security section below.
120
+ - The `sandbox` attribute is resolved from the `source`: `allow-scripts allow-presentation` for any host, plus `allow-same-origin` when the source points at a known video host (YouTube, Vimeo). If your host needs more, append tokens via `additionalSandbox` — see the Security section below.
121
121
  - The iframe sets a fixed `allow` permissions policy (`accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture`) plus `allowfullscreen`, so embedded players can autoplay, go fullscreen, and use picture-in-picture. These are not configurable per-instance.
122
122
  - The iframe renders at a fixed height of 300px. To customize the height, override the iframe styles via a scoped CSS rule targeting `.vc-video__container iframe`.
123
123
  - The placeholder has a height of 200px so the layout does not collapse when no source is provided.
@@ -125,21 +125,31 @@ When `source` is not provided, VcVideo renders a centered film icon placeholder
125
125
  ## Accessibility
126
126
 
127
127
  - The iframe uses the `title` attribute (set to `label` or "Video") for screen readers
128
- - `sandbox` attribute defaults to the secure minimum `allow-scripts allow-presentation`
128
+ - `sandbox` is resolved per source: `allow-scripts allow-presentation` always, plus `allow-same-origin` for known video hosts
129
129
  - `loading="lazy"` defers iframe load until visible
130
130
  - Placeholder state uses `role="img"` with `aria-label="No video source"`
131
131
 
132
132
  ## Security
133
133
 
134
- The iframe `sandbox` defaults to `allow-scripts allow-presentation` — the minimum that keeps mainstream embeds (YouTube, Vimeo) working while denying the framed page access to the parent origin. To add capabilities a specific host needs, pass them through `additionalSandbox`:
134
+ The iframe `sandbox` is resolved from the `source`, not fixed:
135
+
136
+ | Source | Resolved sandbox |
137
+ | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
138
+ | Known video host (youtube.com, youtube-nocookie.com, youtu.be, vimeo.com, and their subdomains) | `allow-scripts allow-presentation allow-same-origin` |
139
+ | Any other host | `allow-scripts allow-presentation` |
140
+ | Relative or malformed URL | `allow-scripts allow-presentation` |
141
+
142
+ Mainstream players need their own origin to start up: without `allow-same-origin` the framed document gets an opaque origin and YouTube's bootstrap throws on storage access before it can build the player. Restricting that token to an allowlist keeps playback working without loosening the sandbox for arbitrary sources.
143
+
144
+ To add capabilities a specific host needs, pass them through `additionalSandbox`:
135
145
 
136
146
  ```vue
137
147
  <!-- Some embeds need popups for auth flows -->
138
148
  <VcVideo source="https://example.com/embed" additional-sandbox="allow-popups" />
139
149
  ```
140
150
 
141
- !!! danger "Never combine `allow-same-origin` with `allow-scripts`"
142
- Per MDN and OWASP, enabling both tokens lets the embedded page remove its own sandbox and reach the parent origin's cookies, `localStorage`, and DOM. `VcVideo` renders whatever `source` you pass, so for untrusted or user-supplied URLs do **not** add `allow-same-origin`.
151
+ !!! danger "Never add `allow-same-origin` for a source on your own origin"
152
+ `allow-same-origin` combined with `allow-scripts` is only dangerous when the framed document shares the host page's origin — then its script can reach `parent.document` and remove the sandbox attribute. For a cross-origin video host the Same-Origin Policy already blocks that, which is why the allowlist above is safe. `VcVideo` renders whatever `source` you pass, so never add the token via `additionalSandbox` for user-supplied URLs or for assets served from the app's own origin.
143
153
 
144
154
  !!! warning "Always use embed URLs, not watch URLs"
145
155
  YouTube watch URLs (`youtube.com/watch?v=...`) will be blocked by the browser's frame policy. Always convert to the embed format (`youtube.com/embed/VIDEO_ID`). Vimeo similarly requires `player.vimeo.com/video/VIDEO_ID`.
@@ -156,7 +166,7 @@ YouTube watch URLs (`youtube.com/watch?v=...`) will be blocked by the browser's
156
166
 
157
167
  - VcVideo lives in `framework/ui/components/atoms/vc-video/`.
158
168
  - The component is a thin wrapper around a native `<iframe>` — no custom video controls are implemented.
159
- - The `sandbox` attribute is computed from a secure base (`allow-scripts allow-presentation`) plus any `additionalSandbox` tokens, deduped. `allow-same-origin` is intentionally omitted from the default — see the Security section.
169
+ - The `sandbox` attribute is computed from a base (`allow-scripts allow-presentation`), plus `allow-same-origin` when `isTrustedEmbedHost(source)` matches `TRUSTED_EMBED_HOSTS`, plus any `additionalSandbox` tokens, deduped. Host matching is exact or on a dot boundary, so `evil-youtube.com` does not match; an unparseable URL falls back to the strict base — see the Security section.
160
170
  - The label is rendered via `VcLabel` (internal atom) with the `tooltip` prop forwarded as the VcLabel tooltip slot content.
161
171
  - Placeholder state (`source` is falsy) swaps the iframe for a `<div>` with `role="img"` containing a `VcIcon` with `lucide-film`.
162
172
 
@@ -285,6 +285,10 @@ clickHandler: () => { navigate(); return true; }
285
285
  - The last visible item carries `aria-current="page"` to indicate the current location.
286
286
  - Separator characters are marked `aria-hidden="true"` so screen readers skip them.
287
287
  - When items overflow, the dropdown is accessible via the trigger button with standard keyboard interaction (Enter/Space to open).
288
+ - An item with an icon and no `title` (a back control, for example) would otherwise have no accessible name — its button falls back to a localized `aria-label` ("Back" / "Zurück"). Items with a visible title set no `aria-label`, so the name is never duplicated.
289
+
290
+ !!! tip "Prefer a real title over relying on the fallback"
291
+ The fallback keeps an icon-only item usable, but "Back" says less than the destination does. Pass a `title` whenever you know it — screen-reader users then hear where the control leads, and sighted users get the truncation tooltip.
288
292
 
289
293
  ## Related Components
290
294
 
@@ -687,6 +687,7 @@ VcInput blocks the minus key for both `number` and `integer` types. If you need
687
687
  | `required` | `boolean` | `false` | Adds a red asterisk to the label (visual only, no validation) |
688
688
  | `loading` | `boolean` | `false` | Shows a spinning loader icon inside the field |
689
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"`) |
690
691
  | `error` | `boolean` | `false` | Activates error styling (red border and ring) |
691
692
  | `errorMessage` | `string` | -- | Error text shown below the field; also activates error styling when truthy |
692
693
  | `debounce` | `string \| number` | -- | Delay in ms before emitting model updates |
@@ -109,16 +109,20 @@ When `expanded` is `false`, the menu shows only icons and letter abbreviations.
109
109
 
110
110
  ## CSS Variables
111
111
 
112
- | Variable | Default | Description |
113
- | --------------- | ------- | ---------------------- |
114
- | `--vc-menu-gap` | `8px` | Gap between menu items |
112
+ | Variable | Default | Description |
113
+ | --------------------------------- | -------------------- | --------------------------------------- |
114
+ | `--vc-menu-gap` | `8px` | Gap between menu items |
115
+ | `--vc-menu-item-focus-ring-color` | `var(--primary-500)` | Focus ring color on keyboard navigation |
115
116
 
116
117
  ## Accessibility
117
118
 
118
- - Menu items are keyboard-focusable
119
- - Groups expand/collapse with animated transitions
120
- - Active state is explicit via the `active` prop on `VcMenuItem`
121
- - Collapsed mode shows tooltips for discoverability
119
+ - `VcMenuItem` renders its interactive row as a native `<button type="button">`, so it is in the tab order and the browser maps Enter/Space to activation. There is no `tabindex` juggling and no `keydown` handler to keep in sync.
120
+ - Focus ring on `:focus-visible` only (not on mouse click), colored via `--vc-menu-item-focus-ring-color`
121
+ - The active item exposes `aria-current="page"`, so assistive technology announces the current location — `active` drives both the styling and the attribute
122
+ - When the menu is collapsed to icons the visible title is hidden, so the button carries `aria-label` with the item title; expanded items rely on their visible text instead and set no `aria-label`
123
+ - Icons and letter abbreviations are `aria-hidden="true"` — they are decorative next to the accessible name
124
+ - `VcMenuGroup` with `variant="section"` renders a native button that reports `aria-expanded` and points `aria-controls` at the children wrapper it toggles
125
+ - Collapsed mode shows tooltips for discoverability (in addition to, not instead of, the accessible name)
122
126
 
123
127
  ## Related Components
124
128
 
@@ -17,13 +17,13 @@ The composables follow a PrimeVue-inspired pattern: each returns reactive state
17
17
 
18
18
  ### Data & State
19
19
 
20
- | Composable | Purpose |
21
- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
22
- | `useDataTableState` | Persists column state (v2 schema: weights, order, hidden/shown IDs) to localStorage/sessionStorage. Auto-migrates v1 (pixel-based) state on first load. Key format: `VC_DATATABLE_{KEY}`. Debounced auto-save (150ms) with restore-on-mount. |
23
- | `useTableColumns` | Column ordering, width management via `columnState` (weight store), computed pixel widths via `engineOutput`. Exposes `recompute()` to trigger a recalculation pass. Watches `visibleColumns` and appends new columns without dropping hidden ones. |
24
- | `useColumnWidthEngine` | Pure functions for deterministic column width computation (see below). |
25
- | `useDataProcessing` | Client-side sort pipeline (single/multi) and row grouping. Skipped when `lazy: true` (server-side). |
26
- | `useTableContext` | Provides/injects table-level context for sub-components. |
20
+ | Composable | Purpose |
21
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
22
+ | `useDataTableState` | Persists column state (v2 schema: weights, order, hidden/shown IDs, `userSized`) to localStorage/sessionStorage. Auto-migrates v1 (pixel-based) state on first load. Key format: `VC_DATATABLE_{KEY}`. Debounced auto-save (150ms) with restore-on-mount. |
23
+ | `useTableColumns` | Column ordering, width management via `columnState` (weight store), computed pixel widths via `engineOutput`. Exposes `recompute()` to trigger a recalculation pass, which also re-derives weights from `VcColumn` props until `markSizingCustomized()` is called. Watches `visibleColumns` and appends new columns without dropping hidden ones. |
24
+ | `useColumnWidthEngine` | Pure functions for deterministic column width computation (see below). |
25
+ | `useDataProcessing` | Client-side sort pipeline (single/multi) and row grouping. Skipped when `lazy: true` (server-side). |
26
+ | `useTableContext` | Provides/injects table-level context for sub-components. |
27
27
 
28
28
  ### Sorting & Filtering
29
29
 
@@ -114,12 +114,12 @@ Mutates `specs` in place so that the weights of `visibleIds` sum to 1.0. Called
114
114
 
115
115
  ### When weights update
116
116
 
117
- | User action | Weight change |
118
- | --------------------------- | --------------------------------------------------------------------------------------- |
119
- | Column resize (drag border) | Dragged column and right neighbor exchange weight proportionally |
120
- | Column show/hide | Hidden column's weight is preserved; shown column uses saved or initial weight |
121
- | Reset columns | All weights rebuilt from declarative `width` props |
122
- | Container resize | Weights unchanged; engine recomputes px widths from same weights × new `availableWidth` |
117
+ | User action | Weight change |
118
+ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
119
+ | Column resize (drag border) | Dragged column and right neighbor exchange weight proportionally; sizing becomes **customized** (`userSized` persisted) |
120
+ | Column show/hide | Hidden column's weight is preserved; shown column uses saved or initial weight |
121
+ | Reset columns | All weights rebuilt from declarative `width` props; sizing returns to declarative mode |
122
+ | Container resize | Declarative mode: weights re-derived from `VcColumn` props at the new width (declared px stay exact). Customized: weights unchanged, px scale with it |
123
123
 
124
124
  ## Usage
125
125
 
@@ -780,6 +780,16 @@ const expandedRows = ref<Order[]>([]);
780
780
  </script>
781
781
  ```
782
782
 
783
+ ### On mobile
784
+
785
+ Expandable rows work the same way in the mobile card view — no extra markup. The `expander` column itself is not rendered as a card field (special columns never are); instead each expandable card gets a chevron toggle on its right edge, and the `#expansion` slot renders inside the card, below the card body. Expansion state is shared with the desktop view, so `expanded-rows` behaves identically on both.
786
+
787
+ Two mobile-specific details:
788
+
789
+ - Tapping the chevron does not trigger `row-click`, so opening the details of a row and expanding it stay separate gestures.
790
+ - The card adds **no padding** around the expansion — the slot content owns its own spacing. Screen width is scarce on a phone, and slot content usually carries padding already, so anything added here would inset it twice.
791
+ - The expansion area scrolls horizontally when its content is wider than the card. Slot content laid out for desktop widths therefore stays reachable on a phone, though a layout that reflows for narrow screens reads better.
792
+
783
793
  ---
784
794
 
785
795
  ## Row Grouping
@@ -1043,7 +1053,9 @@ Persist column widths, column order, and column visibility across page reloads.
1043
1053
 
1044
1054
  **Storage key format:** `VC_DATATABLE_PRODUCT-LIST` (uppercased `state-key`).
1045
1055
 
1046
- **Schema version:** The persisted state uses the **v2 schema**, which stores column weights, column order, and hidden/shown column IDs. `containerWidth` is no longer stored because weights are container-independent — the engine recomputes pixel values from weights on every mount.
1056
+ **Schema version:** The persisted state uses the **v2 schema**, which stores column weights, column order, hidden/shown column IDs, and a `userSized` flag. `containerWidth` is not stored because weights are container-independent — the engine recomputes pixel values from weights on every mount.
1057
+
1058
+ **`userSized` and declarative widths:** until the user actually resizes a column, weights are treated as _declarative_ — re-derived from the `VcColumn` `width`/`minWidth`/`maxWidth` props at the current container width on every recompute. This heals weights captured at a transient width (blades animate their `width` for ~300ms, so the first measurement often happens mid-animation) and means `width="60"` renders as 60px at any blade width. A mouse resize sets `userSized: true`: from then on the saved weights are the user's data — restored as-is and scaled proportionally when the blade width changes. Restored states without the flag (including states saved by older versions) stay declarative; column order and hidden/shown IDs are restored either way. **Reset columns** returns the table to the declarative mode.
1047
1059
 
1048
1060
  If an older browser tab wrote **v1** state (pixel-based widths), it is automatically migrated to v2 on first load. No manual migration is needed.
1049
1061
 
@@ -1658,20 +1670,20 @@ function onRowRemove(event: { data: Product; index: number; cancel: () => void }
1658
1670
 
1659
1671
  ## VcDataTable Slots Reference
1660
1672
 
1661
- | Slot | Props | Description |
1662
- | ----------------------- | --------------------------------------------------------------- | ---------------------------------------------------------- |
1663
- | `default` | -- | VcColumn declarations (required). |
1664
- | `header` | -- | Custom header content above the table. |
1665
- | `footer` | -- | Custom footer content below the table body. |
1666
- | `search-header-actions` | -- | Extra buttons in the search toolbar (beside filter icon). |
1667
- | `selection-banner` | `{ count, totalCount, isSelectAll, selectAll, clearSelection }` | Custom selection banner. |
1668
- | `expansion` | `{ data: T, index: number }` | Content rendered below an expanded row. |
1669
- | `empty` | -- | Custom empty state (no items, no search). |
1670
- | `not-found` | -- | Custom not-found state (no items + active search/filters). |
1671
- | `loading` | -- | Custom loading state. |
1672
- | `groupheader` | `{ data: T, index: number }` | Custom row group header. |
1673
- | `groupfooter` | `{ data: T, index: number }` | Custom row group footer. |
1674
- | `pagination` | `{ pages, currentPage, onPageClick }` | Custom pagination replacing built-in VcPagination. |
1673
+ | Slot | Props | Description |
1674
+ | ----------------------- | --------------------------------------------------------------- | ------------------------------------------------------------ |
1675
+ | `default` | -- | VcColumn declarations (required). |
1676
+ | `header` | -- | Custom header content above the table. |
1677
+ | `footer` | -- | Custom footer content below the table body. |
1678
+ | `search-header-actions` | -- | Extra buttons in the search toolbar (beside filter icon). |
1679
+ | `selection-banner` | `{ count, totalCount, isSelectAll, selectAll, clearSelection }` | Custom selection banner. |
1680
+ | `expansion` | `{ data: T, index: number }` | Content rendered below an expanded row (desktop and mobile). |
1681
+ | `empty` | -- | Custom empty state (no items, no search). |
1682
+ | `not-found` | -- | Custom not-found state (no items + active search/filters). |
1683
+ | `loading` | -- | Custom loading state. |
1684
+ | `groupheader` | `{ data: T, index: number }` | Custom row group header. |
1685
+ | `groupfooter` | `{ data: T, index: number }` | Custom row group footer. |
1686
+ | `pagination` | `{ pages, currentPage, onPageClick }` | Custom pagination replacing built-in VcPagination. |
1675
1687
 
1676
1688
  ---
1677
1689