@vc-shell/vc-app-skill 2.0.11 → 2.1.0-pr248.6401a93

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.0.11",
3
+ "version": "2.1.0-pr248.6401a93",
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.0.11
1
+ 2.1.0
@@ -1 +1 @@
1
- Synced from framework at commit f2225cc27 on 2026-06-22T15:40:33.524Z
1
+ Synced from framework at commit 3a32dc437 on 2026-06-22T17:27:53.738Z
@@ -1032,7 +1032,7 @@ async function loadNextPage() {
1032
1032
  !!! tip "Use unique state keys"
1033
1033
  Every table in your application must have a distinct `state-key`. Two tables sharing the same key will silently overwrite each other's persisted column widths, order, and hidden/shown column lists.
1034
1034
 
1035
- Persist column widths, column order, and column visibility across page reloads. Column layout is stored in `localStorage`/`sessionStorage` (keyed by `state-key`). Sort, search, and the current page are persisted separately — to the **URL query** automatically when the table is rendered inside a URL-addressable blade (see [URL query persistence](#url-query-persistence) below). Selection, column filters, and other transient state are session-scoped and are deliberately excluded from any persistence:
1035
+ Persist column widths, column order, and column visibility across page reloads. Column layout is stored in `localStorage`/`sessionStorage` (keyed by `state-key`). Sort, search, and the current page are NOT persisted by the table itself -- use the state composables with a matching `stateKey` instead (see [URL query persistence](#url-query-persistence) below). Selection, column filters, and other transient state are session-scoped and are deliberately excluded from any persistence:
1036
1036
 
1037
1037
  ```vue
1038
1038
  <VcDataTable :items="products" state-key="product-list">
@@ -1067,58 +1067,78 @@ Listen to state events:
1067
1067
 
1068
1068
  ### URL query persistence
1069
1069
 
1070
- When a table is rendered inside a URL-addressable blade (a workspace or routable blade whose address appears in the address bar), its sort, search, and current page are persisted to the URL query string. Reloading the page or sharing the link restores that view.
1070
+ Sort, search, and the current page are owned by the state composables, not by `VcDataTable`. The `state-key` prop on `VcDataTable` controls column-layout persistence (localStorage/sessionStorage) only.
1071
1071
 
1072
- Writing the view state to the URL is automatic you do not wire it up. Tables outside a blade (or in a blade with no URL) do not persist (no-op). Reading it back on reload is done by the page (see below).
1072
+ To persist query state to the blade URL, use `useDataTableSort`, `useTableSearch`, and `useDataTablePagination` with a matching `stateKey`. Each composable restores its slice from the URL on creation and persists changes via `router.replace` (no history entries added).
1073
1073
 
1074
- - **Namespacing:** query keys are prefixed with the table's `state-key` if set, otherwise with the blade's URL segment — e.g. `?offers_sort=name:DESC&offers_search=foo&offers_page=2`. Give each table a unique `state-key` when several tables can be visible at once (the same rule as column-layout persistence).
1075
- - **Keys:** `<ns>_sort` (`field:ASC` / `field:DESC`), `<ns>_search`, `<ns>_page` (1-based).
1076
- - **History:** changes use `router.replace`, so they do not add browser-history entries.
1077
- - **Scope:** only sort, search, and page. Column layout still uses `localStorage`/`sessionStorage` (above); column filters and selection are not persisted.
1074
+ - **Namespacing:** URL keys are prefixed with the `stateKey` value -- e.g. `?offers_list_sort=name:DESC&offers_list_search=foo&offers_list_page=2`. Use the same string for all three composables on a given page.
1075
+ - **Keys:** `<stateKey>_sort` (`field:ASC` / `field:DESC`), `<stateKey>_search`, `<stateKey>_page` (1-based).
1076
+ - **Scope:** sort, search, and page only. Column layout still uses localStorage/sessionStorage; column filters and selection are not persisted.
1078
1077
 
1079
- The page reads the persisted values with `useTableQueryState(stateKey)`, seeds its sort/search/page refs in `setup`, and loads once. The table does not push the restored values back into the page, so a reload runs a single request rather than one per restored field.
1078
+ Canonical list-page pattern:
1080
1079
 
1081
- ```ts
1082
- import { ref, watch } from "vue";
1083
- import { useDataTableSort, useTableQueryState, useFunctions } from "@vc-shell/framework";
1080
+ ```vue
1081
+ <script setup lang="ts">
1082
+ import { ref, computed, watch, onMounted } from "vue";
1083
+ import { useDataTableSort, useDataTablePagination, useTableSearch } from "@vc-shell/framework";
1084
+
1085
+ const STATE_KEY = "offers_list";
1084
1086
 
1085
- const PAGE_SIZE = 20;
1086
- const { debounce } = useFunctions();
1087
1087
  const { sortField, sortOrder, sortExpression } = useDataTableSort({
1088
+ stateKey: STATE_KEY,
1088
1089
  initialField: "createdDate",
1089
1090
  initialDirection: "DESC",
1090
1091
  });
1091
- const searchValue = ref<string>();
1092
- const currentPage = ref(1);
1093
-
1094
- // Restore from the URL, then load once.
1095
- const restored = useTableQueryState("offers_list").read();
1096
- if (restored.sort) {
1097
- const [field, dir] = restored.sort.split(":");
1098
- sortField.value = field;
1099
- sortOrder.value = dir === "DESC" ? -1 : 1;
1100
- }
1101
- if (restored.search) searchValue.value = restored.search;
1102
- if (restored.page) currentPage.value = restored.page;
1092
+ const { searchValue } = useTableSearch({ stateKey: STATE_KEY });
1093
+
1094
+ const searchResult = ref<{ results: Offer[]; totalCount: number }>();
1095
+ const totalCount = computed(() => searchResult.value?.totalCount ?? 0);
1103
1096
 
1104
- function load() {
1105
- return loadItems({
1097
+ const pagination = useDataTablePagination({
1098
+ stateKey: STATE_KEY,
1099
+ pageSize: 20,
1100
+ totalCount,
1101
+ });
1102
+
1103
+ async function load() {
1104
+ searchResult.value = await api.searchOffers({
1106
1105
  sort: sortExpression.value,
1107
- keyword: searchValue.value || undefined,
1108
- skip: (currentPage.value - 1) * PAGE_SIZE,
1106
+ keyword: searchValue.value,
1107
+ skip: pagination.skip,
1108
+ take: pagination.pageSize,
1109
1109
  });
1110
1110
  }
1111
1111
 
1112
- load();
1113
- watch(searchValue, () => (currentPage.value = 1));
1114
- watch([sortExpression, searchValue, currentPage], debounce(load, 300));
1112
+ onMounted(() => load());
1113
+ watch(sortExpression, () => load());
1114
+ </script>
1115
1115
 
1116
- function onPaginationClick(page: number) {
1117
- currentPage.value = page;
1118
- }
1116
+ <template>
1117
+ <VcDataTable
1118
+ :items="searchResult?.results ?? []"
1119
+ :total-count="pagination.totalCount"
1120
+ :pagination="pagination"
1121
+ v-model:sort-field="sortField"
1122
+ v-model:sort-order="sortOrder"
1123
+ v-model:search-value="searchValue"
1124
+ @pagination-click="pagination.goToPage"
1125
+ state-key="offers_list"
1126
+ >
1127
+ <VcColumn
1128
+ id="name"
1129
+ header="Name"
1130
+ sortable
1131
+ />
1132
+ <VcColumn
1133
+ id="createdDate"
1134
+ header="Created"
1135
+ sortable
1136
+ />
1137
+ </VcDataTable>
1138
+ </template>
1119
1139
  ```
1120
1140
 
1121
- If a blade cannot be reopened on reload (e.g. a non-routable blade, or an intermediate blade that is not on the restored URL path), its table query params have no owner — they are automatically dropped from the URL once the restored blades have mounted, so the address bar never accumulates stale query keys.
1141
+ The `state-key` on `VcDataTable` here persists column layout to localStorage; the `stateKey` passed to the composables persists query state to the URL. Both use the same string value but serve different purposes.
1122
1142
 
1123
1143
  ---
1124
1144
 
@@ -29,6 +29,7 @@ import { useDataTablePagination } from "@vc-shell/framework";
29
29
  const pagination = useDataTablePagination({
30
30
  pageSize: 20,
31
31
  totalCount: computed(() => searchResult.value?.totalCount ?? 0),
32
+ onPageChange: ({ skip }) => loadItems({ ...searchQuery.value, skip }),
32
33
  });
33
34
  ```
34
35
 
@@ -36,30 +37,29 @@ const pagination = useDataTablePagination({
36
37
  <VcDataTable :items="items" :total-count="pagination.totalCount" :pagination="pagination" @pagination-click="pagination.goToPage" />
37
38
  ```
38
39
 
39
- > **Note:** Let `@pagination-click` only update the page (`goToPage`) and load from one watcher that reads `pagination.skip` together with sort and search — see the recipe below. `onPageChange` (load on each page change) still works for simple tables, but do not combine it with that watcher or the page loads twice.
40
-
41
40
  ## API
42
41
 
43
42
  ### Parameters (Options)
44
43
 
45
- | Option | Type | Default | Description |
46
- | -------------- | ------------------------------------------------- | ----------- | ------------------------------------------------ |
47
- | `totalCount` | `MaybeRefOrGetter<number>` | _required_ | Total item count from API response |
48
- | `pageSize` | `MaybeRefOrGetter<number>` | `20` | Items per page |
49
- | `onPageChange` | `(state: { page: number; skip: number }) => void` | `undefined` | Event callback fired when `goToPage()` is called |
44
+ | Option | Type | Default | Description |
45
+ | -------------- | ------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------- |
46
+ | `stateKey` | `string \| undefined` | `undefined` | When set, restores and persists the current page to the blade URL query under this key |
47
+ | `totalCount` | `MaybeRefOrGetter<number>` | _required_ | Total item count from API response |
48
+ | `pageSize` | `MaybeRefOrGetter<number>` | `20` | Items per page |
49
+ | `onPageChange` | `(state: { page: number; skip: number }) => void` | `undefined` | Event callback fired when `goToPage()` is called |
50
50
 
51
51
  ### Returns (`reactive()` object)
52
52
 
53
- | Property | Type | Description |
54
- | ------------- | ------------------------ | ----------------------------------------------------------------------- |
55
- | `currentPage` | `number` | Current 1-based page number (writable) |
56
- | `pages` | `number` (readonly) | Total number of pages |
57
- | `skip` | `number` (readonly) | Current skip offset for API calls |
58
- | `pageSize` | `number` (readonly) | Resolved page size |
59
- | `totalCount` | `number` (readonly) | Resolved total item count |
60
- | `goToPage` | `(page: number) => void` | Navigate to page; fires `onPageChange` |
61
- | `setPage` | `(page: number) => void` | Seed the page WITHOUT firing `onPageChange` use for pull-restore seed |
62
- | `reset` | `() => void` | Reset to page 1; does NOT fire `onPageChange` |
53
+ | Property | Type | Description |
54
+ | ------------- | ------------------------ | ---------------------------------------------------------------------------- |
55
+ | `currentPage` | `number` | Current 1-based page number (writable) |
56
+ | `pages` | `number` (readonly) | Total number of pages |
57
+ | `skip` | `number` (readonly) | Current skip offset for API calls |
58
+ | `pageSize` | `number` (readonly) | Resolved page size |
59
+ | `totalCount` | `number` (readonly) | Resolved total item count |
60
+ | `goToPage` | `(page: number) => void` | Navigate to page; fires `onPageChange` |
61
+ | `setPage` | `(page: number) => void` | Set the page without firing `onPageChange` (used to seed from a URL restore) |
62
+ | `reset` | `() => void` | Reset to page 1; does NOT fire `onPageChange` |
63
63
 
64
64
  All properties are auto-unwrapped by `reactive()` — no `.value` access needed in script or template.
65
65
 
@@ -67,43 +67,38 @@ All properties are auto-unwrapped by `reactive()` — no `.value` access needed
67
67
 
68
68
  ```vue
69
69
  <script setup lang="ts">
70
- import { ref, computed, watch } from "vue";
71
- import { useDataTablePagination, useDataTableSort, useTableQueryState } from "@vc-shell/framework";
70
+ import { ref, computed, watch, onMounted } from "vue";
71
+ import { useDataTablePagination, useDataTableSort } from "@vc-shell/framework";
72
+
73
+ const STATE_KEY = "products_list";
72
74
 
73
75
  // Data layer
74
76
  const searchResult = ref<{ results: Item[]; totalCount: number }>();
75
77
 
76
- async function loadItems(query: { sort?: string; keyword?: string; skip?: number }) {
77
- searchResult.value = await api.search({ take: 20, ...query });
78
+ async function loadItems() {
79
+ searchResult.value = await api.search({
80
+ sort: sortExpression.value,
81
+ skip: pagination.skip,
82
+ take: pagination.pageSize,
83
+ });
78
84
  }
79
85
 
80
- // State only — no self-loading callback.
81
- const pagination = useDataTablePagination({
82
- pageSize: 20,
83
- totalCount: computed(() => searchResult.value?.totalCount ?? 0),
84
- });
86
+ // Sort state
85
87
  const { sortField, sortOrder, sortExpression } = useDataTableSort({
88
+ stateKey: STATE_KEY,
86
89
  initialField: "createdDate",
87
90
  initialDirection: "DESC",
88
91
  });
89
- const searchValue = ref<string>();
90
-
91
- // Restore from the URL, then load once. setPage seeds the page without loading.
92
- const restored = useTableQueryState("product-list").read();
93
- if (restored.sort) {
94
- const [field, dir] = restored.sort.split(":");
95
- sortField.value = field;
96
- sortOrder.value = dir === "DESC" ? -1 : 1;
97
- }
98
- if (restored.search) searchValue.value = restored.search;
99
- if (restored.page) pagination.setPage(restored.page);
100
-
101
- // One loader reading sort + search + page.
102
- watch(
103
- () => ({ sort: sortExpression.value, keyword: searchValue.value || undefined, skip: pagination.skip }),
104
- (query) => loadItems(query),
105
- { immediate: true },
106
- );
92
+
93
+ // Pagination state
94
+ const pagination = useDataTablePagination({
95
+ stateKey: STATE_KEY,
96
+ pageSize: 20,
97
+ totalCount: computed(() => searchResult.value?.totalCount ?? 0),
98
+ });
99
+
100
+ onMounted(() => loadItems());
101
+ watch(sortExpression, () => loadItems());
107
102
  </script>
108
103
 
109
104
  <template>
@@ -112,12 +107,6 @@ watch(
112
107
  :total-count="pagination.totalCount"
113
108
  :pagination="pagination"
114
109
  @pagination-click="pagination.goToPage"
115
- @search="
116
- (kw) => {
117
- searchValue = kw;
118
- pagination.setPage(1);
119
- }
120
- "
121
110
  v-model:sort-field="sortField"
122
111
  v-model:sort-order="sortOrder"
123
112
  >
@@ -151,6 +140,7 @@ export function useOffers() {
151
140
  const pagination = useDataTablePagination({
152
141
  pageSize: 20,
153
142
  totalCount: computed(() => searchResult.value?.totalCount ?? 0),
143
+ onPageChange: ({ skip }) => loadOffers({ ...searchQuery.value, skip }),
154
144
  });
155
145
 
156
146
  return {
@@ -161,7 +151,7 @@ export function useOffers() {
161
151
  }
162
152
  ```
163
153
 
164
- The blade owns the single loader watch (reading `pagination.skip` with sort/search) and binds:
154
+ Blade then simply binds:
165
155
 
166
156
  ```vue
167
157
  <VcDataTable :total-count="pagination.totalCount" :pagination="pagination" @pagination-click="pagination.goToPage" />
@@ -170,12 +160,12 @@ The blade owns the single loader watch (reading `pagination.skip` with sort/sear
170
160
  ## Details
171
161
 
172
162
  - **`reactive()` return**: The composable wraps its return in `reactive()`, so all `Ref`/`ComputedRef` properties are auto-unwrapped. Access with `pagination.pages` (not `pagination.pages.value`). This allows the object to be passed directly as `:pagination` prop to VcDataTable without intermediate conversion.
173
- - **Event callback, not load**: `onPageChange` is an event notification (like VueUse's `useOffsetPagination`). The composable does not fetch data. With a single loader watcher you usually omit it and read `pagination.skip` from the watcher instead.
174
- - **`setPage` vs `goToPage`**: `goToPage(n)` updates the page and fires `onPageChange`; `setPage(n)` updates it without the callback. Use `setPage` to seed the page from a URL restore so the seed itself does not load.
163
+ - **Event callback, not load**: `onPageChange` is an event notification (like VueUse's `useOffsetPagination`). The composable does not know about data fetching -- it just notifies.
175
164
  - **No auto-clamp**: When `totalCount` decreases (e.g. after deletion), `currentPage` is NOT automatically clamped. Call `reset()` or `goToPage(1)` explicitly after mutations.
176
165
  - **reset() is silent**: `reset()` sets `currentPage = 1` but does NOT fire `onPageChange`. This prevents accidental double-fetches when the consumer resets pagination during a reload.
177
- - **Pure without callback**: Omit `onPageChange` and the composable works as pure state useful for unit tests or when the consumer prefers to watch properties reactively.
166
+ - **Pure without callback**: Omit `onPageChange` and the composable works as pure state -- useful for unit tests or when the consumer prefers to watch properties reactively.
178
167
  - **Why `reactive()` and not `ref()`**: Pagination is a cohesive group of properties always used together (`pagination.xxx`). `reactive()` is the Vue-idiomatic choice for such objects. `useDataTableSort` returns `ref()`s because its properties are destructured and used with `v-model` individually.
168
+ - **URL state (stateKey)**: When `stateKey` is provided, the composable reads the current page from the blade URL query on creation (via `setPage`, which does not fire `onPageChange`) and persists it on every `goToPage` call. Without `stateKey`, behavior is unchanged.
179
169
 
180
170
  ## Tips
181
171
 
@@ -46,10 +46,11 @@ const { sortField, sortOrder, sortExpression, resetSort } = useDataTableSort({
46
46
 
47
47
  ### Parameters (Options)
48
48
 
49
- | Option | Type | Default | Description |
50
- | ------------------ | ----------------- | ----------- | --------------------------------- |
51
- | `initialField` | `string` | `undefined` | Column field to sort by initially |
52
- | `initialDirection` | `"ASC" \| "DESC"` | `undefined` | Initial sort direction |
49
+ | Option | Type | Default | Description |
50
+ | ------------------ | --------------------- | ----------- | -------------------------------------------------------------------------- |
51
+ | `stateKey` | `string \| undefined` | `undefined` | When set, restores and persists sort to the blade URL query under this key |
52
+ | `initialField` | `string` | `undefined` | Column field to sort by initially |
53
+ | `initialDirection` | `"ASC" \| "DESC"` | `undefined` | Initial sort direction |
53
54
 
54
55
  ### Returns
55
56
 
@@ -74,26 +75,25 @@ When `sortOrder` is `0`, `sortExpression` returns `undefined` regardless of `sor
74
75
 
75
76
  ```vue
76
77
  <script setup lang="ts">
77
- import { ref, watch } from "vue";
78
+ import { ref, watch, onMounted } from "vue";
78
79
  import { useDataTableSort } from "@vc-shell/framework";
79
80
 
80
81
  const { sortField, sortOrder, sortExpression, resetSort } = useDataTableSort({
82
+ stateKey: "products_list",
81
83
  initialField: "createdDate",
82
84
  initialDirection: "DESC",
83
85
  });
84
86
 
85
87
  const items = ref([]);
86
88
  const loading = ref(false);
87
- const currentPage = ref(1);
88
- const PAGE_SIZE = 20;
89
89
 
90
- async function loadItems(query: { sort?: string; skip?: number }) {
90
+ async function loadItems() {
91
91
  loading.value = true;
92
92
  try {
93
93
  const response = await api.searchProducts({
94
- sort: query.sort, // e.g. "createdDate:DESC" or undefined
95
- skip: query.skip ?? 0,
96
- take: PAGE_SIZE,
94
+ sort: sortExpression.value, // e.g. "createdDate:DESC" or undefined
95
+ skip: 0,
96
+ take: 20,
97
97
  });
98
98
  items.value = response.results;
99
99
  } finally {
@@ -101,13 +101,8 @@ async function loadItems(query: { sort?: string; skip?: number }) {
101
101
  }
102
102
  }
103
103
 
104
- // Load from one watcher that reads sort together with search and page, so changing
105
- // several at once is a single request. { immediate: true } also does the first load.
106
- watch(
107
- () => ({ sort: sortExpression.value, skip: (currentPage.value - 1) * PAGE_SIZE }),
108
- (query) => loadItems(query),
109
- { immediate: true },
110
- );
104
+ onMounted(() => loadItems());
105
+ watch(sortExpression, () => loadItems());
111
106
  </script>
112
107
 
113
108
  <template>
@@ -141,13 +136,13 @@ watch(
141
136
  - **Sort expression format**: `sortExpression` returns `"field:DIRECTION"` when both `sortField` and a non-zero `sortOrder` are set, otherwise `undefined`. This format is directly compatible with VirtoCommerce Platform search endpoints.
142
137
  - **Reset behavior**: `resetSort()` restores `sortField` and `sortOrder` to the values passed at construction time. If no initial options were provided, both are cleared.
143
138
  - **Numeric order convention**: `VcDataTable` uses PrimeVue's numeric sort order convention (`1`/`-1`/`0`). This composable encapsulates the mapping so call sites never need to handle the numeric values directly.
144
- - **Stateless regarding data**: The composable only manages the sort field and direction. Read `sortExpression` from the same loader watcher that reads search and page, instead of giving sort its own `watch(sortExpression, load)`. See VcDataTable → URL query persistence.
139
+ - **Stateless regarding data**: The composable only manages the sort field and direction. Combine it with your own data-fetching logic using `watch(sortExpression, ...)`.
140
+ - **URL state (stateKey)**: When `stateKey` is provided, the composable reads its slice from the blade URL query on creation and writes back on every change (via `router.replace`). Without `stateKey`, behavior is unchanged -- `sortField` and `sortOrder` are plain refs with no URL interaction.
145
141
 
146
142
  ## Tips
147
143
 
148
144
  - Always mark columns as `sortable` in your `<VcColumn>` definitions for the sort indicator icon to appear.
149
- - Use `{ immediate: true }` on the combined loader watcher to trigger the initial (or URL-restored) data load with the correct sort on mount.
150
- - On reload, seed `sortField`/`sortOrder` from `useTableQueryState(stateKey).read().sort` (`"field:DIR"`) before the loader watcher, so restore costs a single request.
145
+ - Use `{ immediate: true }` on the `watch(sortExpression, ...)` call to trigger the initial data load with the correct sort on mount.
151
146
  - Call `resetSort()` alongside any "clear all filters" action to keep sort state consistent with the rest of the filter UI.
152
147
 
153
148
  ## Related
@@ -11,21 +11,21 @@ Replace the imperative `VcTable` component (columns array, manual sort handlers,
11
11
 
12
12
  Full prop/event mapping:
13
13
 
14
- | VcTable prop/event | VcDataTable equivalent |
15
- | -------------------------- | ------------------------------------------------ |
16
- | `:columns="columns"` | `<VcColumn>` child components (see RULE 2) |
17
- | `:selected-item-id="id"` | `v-model:active-item-id="id"` |
18
- | `:sort="sortExpression"` | `v-model:sort-field` + `v-model:sort-order` |
19
- | `:multiselect="true"` | `:selection-mode="'multiple'"` |
20
- | `@selection-changed="fn"` | `v-model:selection="ref"` |
21
- | `:search-value="val"` | `:searchable="true"` (internal state) |
22
- | `@search:change="fn"` | `@search="fn"` |
23
- | `@item-click="fn"` | `@row-click="fn"` (different signature) |
24
- | `@header-click="fn"` | Remove (sort is declarative via v-model) |
25
- | `@scroll:ptr="fn"` | `:pull-to-refresh="true"` + `@pull-refresh="fn"` |
26
- | `:pages` + `:current-page` | `:pagination="{ currentPage, pages }"` |
27
- | `:active-filter-count` | Remove (managed by global-filters internally) |
28
- | `<!--@vue-generic {T}-->` | Remove (no longer needed) |
14
+ | VcTable prop/event | VcDataTable equivalent |
15
+ | -------------------------- | ----------------------------------------------------------------------- |
16
+ | `:columns="columns"` | `<VcColumn>` child components (see RULE 2) |
17
+ | `:selected-item-id="id"` | `v-model:active-item-id="id"` |
18
+ | `:sort="sortExpression"` | `v-model:sort-field` + `v-model:sort-order` |
19
+ | `:multiselect="true"` | `:selection-mode="'multiple'"` |
20
+ | `@selection-changed="fn"` | `v-model:selection="ref"` |
21
+ | `:search-value="val"` | `v-model:search-value` bound to `useTableSearch({ stateKey? })` |
22
+ | `@search:change="fn"` | `@search="fn"` (or drop; use watcher on `searchValue`) |
23
+ | `@item-click="fn"` | `@row-click="fn"` (different signature) |
24
+ | `@header-click="fn"` | Remove (sort is declarative via v-model) |
25
+ | `@scroll:ptr="fn"` | `:pull-to-refresh="true"` + `@pull-refresh="fn"` |
26
+ | `:pages` + `:current-page` | `:pagination="pagination"` from `useDataTablePagination({ stateKey? })` |
27
+ | `:active-filter-count` | Remove (managed by global-filters internally) |
28
+ | `<!--@vue-generic {T}-->` | Remove (no longer needed) |
29
29
 
30
30
  **BEFORE:**
31
31
 
@@ -310,7 +310,7 @@ The Props interface keeps `columns` — the parent still passes column definitio
310
310
 
311
311
  ## RULE 3: Sort Composable Replacement
312
312
 
313
- Replace `useTableSort` with `useDataTableSort`. The new composable returns `sortField` and `sortOrder` as separate refs that bind directly to `v-model:sort-field` and `v-model:sort-order`. Remove the `onHeaderClick` handler entirely.
313
+ Replace `useTableSort` with `useDataTableSort`. The new composable returns `sortField` and `sortOrder` as separate refs that bind directly to `v-model:sort-field` and `v-model:sort-order`. Remove the `onHeaderClick` handler entirely. Pass `stateKey` to persist sort to the blade URL (opt-in; omit for no URL persistence).
314
314
 
315
315
  **BEFORE:**
316
316
 
@@ -339,6 +339,7 @@ watch(sortExpression, async () => {
339
339
  import { useDataTableSort } from "@vc-shell/framework";
340
340
 
341
341
  const { sortField, sortOrder, sortExpression } = useDataTableSort({
342
+ stateKey: "my_list", // optional: persists sort to blade URL
342
343
  initialField: "createdDate",
343
344
  initialDirection: "DESC",
344
345
  });
@@ -684,10 +685,14 @@ Key points:
684
685
  - The `@filter` event fires when the user applies filters. The `event.filters` object is keyed by filter `id`, with values matching the selected option values.
685
686
  - The active filter count badge is managed internally by VcDataTable — no need for a manual `activeFilterCount` computed.
686
687
 
687
- ## RULE 8: Pagination — Manual Calculation → useDataTablePagination
688
+ ## RULE 8: Pagination and Search useDataTablePagination + useTableSearch
688
689
 
689
690
  Replace manual `pages`/`currentPage` computed properties and `@pagination-click` handlers with the `useDataTablePagination` composable. The composable manages skip/page math internally and returns a reactive pagination object that VcDataTable consumes directly.
690
691
 
692
+ Pass `stateKey` to persist the current page to the blade URL (opt-in). The table `state-key` prop is column-layout persistence only and does not control URL query state.
693
+
694
+ For search, replace a bare `ref("")` with `useTableSearch({ stateKey? })`. The returned `searchValue` ref binds to `v-model:search-value` on VcDataTable. With `stateKey`, the keyword is persisted to the blade URL. Without `stateKey`, it behaves like a plain ref.
695
+
691
696
  **BEFORE (composable):**
692
697
 
693
698
  ```typescript
@@ -733,6 +738,7 @@ export function useMyList(options?: { pageSize?: number }) {
733
738
  });
734
739
 
735
740
  const pagination = useDataTablePagination({
741
+ stateKey: "my_list", // optional: persists current page to blade URL
736
742
  pageSize,
737
743
  totalCount: computed(() => searchResult.value?.totalCount ?? 0),
738
744
  onPageChange: ({ skip }) => loadItems({ ...searchQuery.value, skip }),
@@ -778,12 +784,15 @@ const { items, pagination, loadItems, loading } = useMyList();
778
784
  - `totalCount`, `pages`, `currentPage` computed from composable return
779
785
  - Manual `onPaginationClick` function in blade pages
780
786
  - Manual `skip` calculation `(page - 1) * pageSize`
787
+ - Any `useTableQueryPersistence` or `useTableQueryState().read()` calls — URL query persistence is now handled by `stateKey` on the composables
781
788
 
782
789
  **What to add:**
783
790
 
784
- - `useDataTablePagination` import in composable
791
+ - `useDataTablePagination` import in composable (with optional `stateKey` for URL persistence)
792
+ - `useTableSearch({ stateKey? })` in the blade to replace a bare `ref("")` for search
785
793
  - `pagination` object in composable return
786
794
  - `:pagination="pagination"` and `@pagination-click="pagination.goToPage"` in template
795
+ - `v-model:search-value="searchValue"` from `useTableSearch` in template
787
796
 
788
797
  ## Verification
789
798