@vc-shell/vc-app-skill 2.0.0-alpha.33-pr220.455e322 → 2.0.0-alpha.34

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/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
1
+ # [2.0.0-alpha.34](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.33...v2.0.0-alpha.34) (2026-04-22)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **datatable:** normalise date-range filter values to YYYY-MM-DD ([d89864a](https://github.com/VirtoCommerce/vc-shell/commit/d89864aa635e7479137fb0ad501197adf335f99e))
7
+ * **migrate:** datatable prompt forbids 'removed filters for green build' shortcut ([af5ae8e](https://github.com/VirtoCommerce/vc-shell/commit/af5ae8e4259b435729e6cea9e4c61d74b41d3952))
8
+ * **migrate:** use-data-table-pagination prompt — per-file scope skip ([8949f01](https://github.com/VirtoCommerce/vc-shell/commit/8949f01da97b69c15fc6e3a2fca951608731979c))
9
+
10
+
11
+ ### chore
12
+
13
+ * **scripts:** normalize yarn scripts per industry standards ([1cdd0cb](https://github.com/VirtoCommerce/vc-shell/commit/1cdd0cb517d2436ef2a509c6b6c358f6a48630d1))
14
+
15
+
16
+ ### Features
17
+
18
+ * **migrate:** add use-data-table-pagination-audit + AI migration prompt ([0c2e7b6](https://github.com/VirtoCommerce/vc-shell/commit/0c2e7b6efba387d9fafe04e5bdcd21ea20500259))
19
+ * **migrate:** expand v2 migration tooling — icon/asset/audit prompts and blade-event cleanup ([f4788d4](https://github.com/VirtoCommerce/vc-shell/commit/f4788d4d9c588157ca5c11facfe558a69c254c2e)), closes [#41](https://github.com/VirtoCommerce/vc-shell/issues/41)
20
+
21
+
22
+ ### BREAKING CHANGES
23
+
24
+ * **scripts:** for external consumers: old script names
25
+ (storybook-serve, build-framework, check-locales etc) are removed.
26
+ Legacy aliases are deliberately not provided — they would perpetuate
27
+ the non-standard naming this commit eliminates.
1
28
  # [2.0.0-alpha.33](https://github.com/VirtoCommerce/vc-shell/compare/v2.0.0-alpha.32...v2.0.0-alpha.33) (2026-04-14)
2
29
 
3
30
  ### Bug Fixes
package/README.md CHANGED
@@ -14,6 +14,7 @@ The skill covers:
14
14
  - Enhancing existing modules with surgical modifications (add columns, fields, toolbar actions, logic, blade links)
15
15
  - Generating full multi-module applications from a free-text prompt (design command)
16
16
  - Promoting prototype modules from mock data to real API clients
17
+ - Migrating existing apps to the latest `@vc-shell/framework` version (runs the CLI migrator, regenerates API clients, completes AI-assisted manual refactors)
17
18
  - Following vc-shell conventions: Vue 3 + TypeScript, Tailwind with `tw-` prefix, `<script setup>`, BEM class names
18
19
 
19
20
  ## Installation
@@ -154,6 +155,28 @@ When you generate a module without an API client, it uses mock data with `// vc-
154
155
  - **Phase 4: Code Transformation** — replaces mock code with real API calls, renames fields, updates locales
155
156
  - **Phase 5: Cleanup** — type-checks, removes prototype marker on success
156
157
 
158
+ ### `/vc-app migrate`
159
+
160
+ Fully automatic migration of an existing app to the latest `@vc-shell/framework` version. Runs the CLI migrator for mechanical transforms, regenerates API clients with the new Interface-style output, installs updated dependencies, and dispatches AI agents to complete manual refactors flagged in `MIGRATION_REPORT.md`.
161
+
162
+ ```
163
+ /vc-app migrate
164
+ ```
165
+
166
+ Flow:
167
+
168
+ - **Step 1: Pre-flight** — verifies the project uses `@vc-shell/framework`; warns on uncommitted changes
169
+ - **Step 2: CLI migrator** — runs `@vc-shell/migrate --update-deps` to apply mechanical transforms and align peer-dependency versions (including `@vc-shell/*`, ESLint, Vite, TypeScript, VueUse, `vue-router` and other curated peer deps)
170
+ - **Step 2.5: API client regeneration** — adds `APP_TYPE_STYLE=Interface`, runs `generate-api-client`, verifies types compile
171
+ - **Step 3: Install** — `yarn install` to refresh the lockfile
172
+ - **Step 4: Parse report** — reads `MIGRATION_REPORT.md`, maps "Manual Migration Required" topics to migration prompts and patterns
173
+ - **Step 5: AI migration** — dispatches `migration-agent` on affected files per topic (supports partial resume via `.vc-app-migrate-state.json`)
174
+ - **Step 6: Verify** — runs `vue-tsc --noEmit` and `yarn build`, iteratively fixes type errors
175
+ - **Step 6.5: Format** — runs Prettier across the project
176
+ - **Step 7: Summary** — updates the report and prints a completion summary with remaining issues
177
+
178
+ Handles topics: widgets, form management (`useBladeForm`), injection-key renames, NSwag class-to-interface, blade props simplification, notifications, VcTable → VcDataTable, icon replacements, assets API, pagination, and a manual-audit catch-all.
179
+
157
180
  ## Update
158
181
 
159
182
  ```bash
@@ -215,17 +238,18 @@ cli/vc-app-skill/
215
238
 
216
239
  The skill dispatches specialized agents for different tasks:
217
240
 
218
- | Agent | Purpose |
219
- | ------------------------- | ----------------------------------------------------------------- |
220
- | `api-analyzer` | Discovers entities and CRUD methods in API client files |
221
- | `list-blade-generator` | Generates list blade + plural composable |
222
- | `details-blade-generator` | Generates details blade + singular composable |
223
- | `locales-generator` | Scans generated files for i18n keys, writes locale JSON |
224
- | `module-assembler` | Creates barrel files and registers module (create + append modes) |
225
- | `type-checker` | Runs vue-tsc, iteratively fixes type errors |
226
- | `promote-agent` | Transforms mock composables/blades/locales to use real API |
227
- | `module-analyzer` | Analyzes existing module structure (read-only) |
228
- | `blade-enhancer` | Surgical edits to existing blades/composables/locales |
241
+ | Agent | Purpose |
242
+ | ------------------------- | -------------------------------------------------------------------------- |
243
+ | `api-analyzer` | Discovers entities and CRUD methods in API client files |
244
+ | `list-blade-generator` | Generates list blade + plural composable |
245
+ | `details-blade-generator` | Generates details blade + singular composable |
246
+ | `locales-generator` | Scans generated files for i18n keys, writes locale JSON |
247
+ | `module-assembler` | Creates barrel files and registers module (create + append modes) |
248
+ | `type-checker` | Runs vue-tsc, iteratively fixes type errors |
249
+ | `promote-agent` | Transforms mock composables/blades/locales to use real API |
250
+ | `migration-agent` | Applies AI-assisted manual migrations on files flagged by the migrate flow |
251
+ | `module-analyzer` | Analyzes existing module structure (read-only) |
252
+ | `blade-enhancer` | Surgical edits to existing blades/composables/locales |
229
253
 
230
254
  ### Running locally
231
255
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vc-shell/vc-app-skill",
3
- "version": "2.0.0-alpha.33-pr220.455e322",
3
+ "version": "2.0.0-alpha.34",
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.0-alpha.33
1
+ 2.0.0-alpha.34
@@ -1 +1 @@
1
- Synced from framework at commit b399787ff on 2026-04-14T15:15:27.844Z
1
+ Synced from framework at commit cec6c9078 on 2026-04-22T09:43:16.584Z
@@ -56,14 +56,14 @@ import { toRef } from "vue";
56
56
 
57
57
  const assets = useAssetsManager(
58
58
  computed({
59
- get: () => entity.value.images ?? [],
59
+ get: () => offer.value.images ?? [],
60
60
  set: (val) => {
61
- entity.value.images = val;
61
+ offer.value.images = val;
62
62
  },
63
63
  }),
64
64
  {
65
- uploadPath: () => `entities/${entity.value?.id ?? "new"}`,
66
- confirmRemove: () => showConfirmation(t("MODULE.ALERTS.IMAGE_DELETE")),
65
+ uploadPath: () => `offers/${offer.value?.id ?? "new"}`,
66
+ confirmRemove: () => showConfirmation(t("OFFERS.ALERTS.IMAGE_DELETE")),
67
67
  },
68
68
  );
69
69
  ```
@@ -22,21 +22,21 @@ import { useBladeWidgets } from "@vc-shell/framework";
22
22
 
23
23
  const { refreshAll } = useBladeWidgets([
24
24
  {
25
- id: "ChildListWidget",
25
+ id: "OffersWidget",
26
26
  icon: "lucide-tag",
27
- title: "MODULE.WIDGETS.CHILD_LIST.TITLE",
28
- badge: childCount,
29
- loading: childLoading,
30
- onClick: () => openBlade({ name: "ChildList" }),
31
- onRefresh: () => reloadChildren(),
27
+ title: "OFFERS.TITLE",
28
+ badge: offersCount,
29
+ loading: offersLoading,
30
+ onClick: () => openBlade({ name: "OffersList" }),
31
+ onRefresh: () => reloadOffers(),
32
32
  },
33
33
  {
34
- id: "NotesWidget",
34
+ id: "ReviewsWidget",
35
35
  icon: "lucide-star",
36
- title: "MODULE.WIDGETS.NOTES.TITLE",
37
- badge: notesCount,
36
+ title: "REVIEWS.TITLE",
37
+ badge: reviewsCount,
38
38
  isVisible: computed(() => !!item.value?.id),
39
- onClick: () => openBlade({ name: "NotesList" }),
39
+ onClick: () => openBlade({ name: "ReviewsList" }),
40
40
  },
41
41
  ]);
42
42
 
@@ -119,7 +119,7 @@ import { MessageWidget } from "./components/widgets";
119
119
  registerExternalWidget({
120
120
  id: "MessageWidget",
121
121
  component: markRaw(MessageWidget),
122
- targetBlades: ["EntityDetails", "AnotherDetails"],
122
+ targetBlades: ["ProductDetails", "OrderDetails"],
123
123
  isVisible: (blade?: BladeDescriptor) => !!blade?.param,
124
124
  });
125
125
  ```
@@ -176,61 +176,61 @@ import { useBladeWidgets } from "@vc-shell/framework";
176
176
  const { refresh, refreshAll } = useBladeWidgets([]);
177
177
 
178
178
  async function save() {
179
- await api.saveEntity(entity.value);
179
+ await api.saveProduct(product.value);
180
180
  refreshAll(); // refresh all widgets (including MessageWidget)
181
181
  // or: refresh("MessageWidget"); // refresh a specific widget by ID
182
182
  }
183
183
  </script>
184
184
  ```
185
185
 
186
- ## Recipe: Entity Detail Blade with Multiple Widgets
186
+ ## Recipe: Product Detail Blade with Multiple Widgets
187
187
 
188
188
  ```vue
189
189
  <script setup lang="ts">
190
190
  import { ref, computed } from "vue";
191
191
  import { useBladeWidgets, defineBladeContext } from "@vc-shell/framework";
192
192
 
193
- const entity = ref({ id: "ent-1", name: "Sample" });
194
- const childCount = ref(0);
195
- const notesCount = ref(0);
196
- const childLoading = ref(false);
193
+ const product = ref({ id: "prod-1", name: "Widget A" });
194
+ const offersCount = ref(0);
195
+ const reviewsCount = ref(0);
196
+ const offersLoading = ref(false);
197
197
 
198
- // Expose entity data to widgets
199
- defineBladeContext(computed(() => ({ id: entity.value?.id })));
198
+ // Expose product data to widgets
199
+ defineBladeContext(computed(() => ({ id: product.value?.id })));
200
200
 
201
- async function reloadChildren() {
202
- childLoading.value = true;
201
+ async function reloadOffers() {
202
+ offersLoading.value = true;
203
203
  try {
204
- const result = await api.searchChildren({ entityId: entity.value.id });
205
- childCount.value = result.totalCount;
204
+ const result = await api.searchOffers({ productId: product.value.id });
205
+ offersCount.value = result.totalCount;
206
206
  } finally {
207
- childLoading.value = false;
207
+ offersLoading.value = false;
208
208
  }
209
209
  }
210
210
 
211
211
  const { refreshAll } = useBladeWidgets([
212
212
  {
213
- id: "ChildListWidget",
213
+ id: "OffersWidget",
214
214
  icon: "lucide-tag",
215
- title: "MODULE.WIDGETS.CHILD_LIST",
216
- badge: childCount,
217
- loading: childLoading,
218
- isVisible: computed(() => !!entity.value?.id),
219
- onClick: () => openBlade({ name: "ChildList" }),
220
- onRefresh: reloadChildren,
215
+ title: "PRODUCT.WIDGETS.OFFERS",
216
+ badge: offersCount,
217
+ loading: offersLoading,
218
+ isVisible: computed(() => !!product.value?.id),
219
+ onClick: () => openBlade({ name: "OffersList" }),
220
+ onRefresh: reloadOffers,
221
221
  },
222
222
  {
223
- id: "NotesWidget",
223
+ id: "ReviewsWidget",
224
224
  icon: "lucide-star",
225
- title: "MODULE.WIDGETS.NOTES",
226
- badge: notesCount,
227
- isVisible: computed(() => !!entity.value?.id),
228
- onClick: () => openBlade({ name: "NotesList" }),
225
+ title: "PRODUCT.WIDGETS.REVIEWS",
226
+ badge: reviewsCount,
227
+ isVisible: computed(() => !!product.value?.id),
228
+ onClick: () => openBlade({ name: "ReviewsList" }),
229
229
  },
230
230
  ]);
231
231
 
232
232
  async function save() {
233
- await api.saveEntity(entity.value);
233
+ await api.saveProduct(product.value);
234
234
  // Refresh all widget counts after saving
235
235
  refreshAll();
236
236
  }
@@ -67,7 +67,7 @@ const isVisuallyExpanded = computed(() => isExpanded.value || isHoverExpanded.va
67
67
 
68
68
  ## Details
69
69
 
70
- - **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 `/my-app/`, the key is `VC_APP_MENU_EXPANDED_my-app`. This allows multiple vc-shell apps on the same domain to maintain independent sidebar states.
70
+ - **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 `/vendor-portal/`, the key is `VC_APP_MENU_EXPANDED_vendor-portal`. This allows multiple vc-shell apps on the same domain to maintain independent sidebar states.
71
71
  - **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.
72
72
  - **Cleanup**: Pending hover timeouts are cleaned up via `onScopeDispose` to prevent memory leaks when the composable's effect scope is destroyed.
73
73
  - **Default state**: The sidebar starts pinned open (`true`) on first visit, which is the most user-friendly default for new users.
@@ -0,0 +1,28 @@
1
+ # usePlatformLocaleSync
2
+
3
+ One-way reactive bridge from the VirtoCommerce platform's locale storage key (`NG_TRANSLATE_LANG_KEY`, set by AngularJS + angular-translate) to the shell's language service.
4
+
5
+ Call this composable only when the shell runs embedded inside the platform — `useShellBootstrap` invokes it automatically when `options.isEmbedded === true`. In standalone mode the shell owns its own locale via `VC_LANGUAGE_SETTINGS`, and this composable should not be used.
6
+
7
+ ## When to Use
8
+
9
+ - Never call directly from feature code. This is a framework-internal sync primitive.
10
+ - It is invoked once per `VcApp` mount from `useShellBootstrap`.
11
+
12
+ ## Behaviour
13
+
14
+ - Reads `localStorage["NG_TRANSLATE_LANG_KEY"]` via VueUse's `useLocalStorage`, which subscribes to `storage` events for cross-tab reactivity.
15
+ - On setup, if the value is non-empty, calls `LanguageService.setLocale(value)`. `setLocale` normalises the value (e.g. `en-US` → `en-us`), falls back to `en` for unsupported locales, updates `vue-i18n`, reconfigures `vee-validate`, and persists to `VC_LANGUAGE_SETTINGS`.
16
+ - On subsequent changes of the platform key, re-applies the value.
17
+ - Skips empty strings (platform clearing the key does not blank the shell locale).
18
+ - Skips values equal to `currentLocale` to avoid redundant re-configuration.
19
+
20
+ ## How It Works
21
+
22
+ `useLocalStorage("NG_TRANSLATE_LANG_KEY", "")` returns a `Ref<string>` that VueUse keeps in sync with `localStorage` and the DOM `storage` event (which fires in tabs other than the writer). The composable applies the current ref value once synchronously and then registers a `watch` on it; any cross-tab mutation flows through the ref into `setLocale`.
23
+
24
+ The watcher is bound to the active effect scope (typically `VcApp`'s setup). When `VcApp` unmounts, the watcher stops; `useLocalStorage` cleans up its own `storage` listener.
25
+
26
+ ## Relationship to `VC_LANGUAGE_SETTINGS`
27
+
28
+ The sync is strictly one-directional. `setLocale` writes to `VC_LANGUAGE_SETTINGS` as a side effect, but this composable never writes to `NG_TRANSLATE_LANG_KEY`. In embedded mode the in-shell `LanguageSelector` is unreachable (it lives inside `UserDropdownButton`, which is hidden when `isEmbedded` is `true`), so there is no competing writer from the shell side.
@@ -88,8 +88,8 @@ const { applySettings } = useSettings();
88
88
 
89
89
  // Override default platform settings with module-specific branding
90
90
  applySettings({
91
- logo: "/modules/my-module/logo.svg",
92
- title: "My Module",
91
+ logo: "/modules/vendor-portal/logo.svg",
92
+ title: "Vendor Portal",
93
93
  });
94
94
  </script>
95
95
  ```
@@ -51,7 +51,7 @@ This is how vc-shell achieves its modular architecture: modules can extend each
51
51
 
52
52
  <!-- Other modules can inject components here -->
53
53
  <ExtensionPoint
54
- name="entity:custom-fields"
54
+ name="seller:commissions"
55
55
  separator
56
56
  gap="1rem"
57
57
  />
@@ -66,14 +66,14 @@ import { ExtensionPoint } from "@vc-shell/framework";
66
66
  **Plugin module** -- registers a component into that extension point:
67
67
 
68
68
  ```typescript
69
- // modules/entity-extensions/index.ts
69
+ // modules/marketplace-commissions/index.ts
70
70
  import { defineAppModule, useExtensionPoint } from "@vc-shell/framework";
71
- import CustomFields from "./components/CustomFields.vue";
71
+ import CommissionFields from "./components/CommissionFields.vue";
72
72
 
73
- const { add } = useExtensionPoint("entity:custom-fields");
73
+ const { add } = useExtensionPoint("seller:commissions");
74
74
  add({
75
- id: "entity-extension",
76
- component: CustomFields,
75
+ id: "marketplace-commission",
76
+ component: CommissionFields,
77
77
  props: { showAdvanced: true },
78
78
  priority: 10,
79
79
  });
@@ -83,7 +83,7 @@ export default defineAppModule({
83
83
  });
84
84
  ```
85
85
 
86
- When the seller details page renders, `CustomFields` appears automatically below the main form -- with a separator and 1rem gap.
86
+ When the seller details page renders, `CommissionFields` appears automatically below the main form -- with a separator and 1rem gap.
87
87
 
88
88
  ---
89
89
 
@@ -97,9 +97,9 @@ Extension points follow a two-role architecture:
97
97
  HOST (declares) PLUGIN (registers)
98
98
  ----------------- ------------------
99
99
  "I have a slot called "I want to put my
100
- entity:custom-fields CustomFields
100
+ seller:commissions CommissionFields
101
101
  where plugins can component in the
102
- inject content." entity:custom-fields slot."
102
+ inject content." seller:commissions slot."
103
103
 
104
104
  | |
105
105
  v v
@@ -116,8 +116,8 @@ Neither side imports the other. They communicate through a shared **name string*
116
116
 
117
117
  Plugins can register components **before** the host declares the extension point. The reactive store handles this gracefully:
118
118
 
119
- 1. Plugin calls `useExtensionPoint("entity:custom-fields")` and `add(...)` -- the store creates an undeclared entry and stores the component.
120
- 2. Later, host calls `defineExtensionPoint("entity:custom-fields")` -- the store upgrades the entry to "declared" and preserves all previously registered components.
119
+ 1. Plugin calls `useExtensionPoint("seller:commissions")` and `add(...)` -- the store creates an undeclared entry and stores the component.
120
+ 2. Later, host calls `defineExtensionPoint("seller:commissions")` -- the store upgrades the entry to "declared" and preserves all previously registered components.
121
121
  3. The host's `components` computed ref reactively picks up the registered components.
122
122
 
123
123
  This means module load order does not matter. Remote modules loaded via Module Federation may install in any sequence, and extensions still work.
@@ -165,7 +165,7 @@ Used in pages or components that **accept** plugin content. Declares the extensi
165
165
  ```typescript
166
166
  import { defineExtensionPoint } from "@vc-shell/framework";
167
167
 
168
- const { components, hasComponents } = defineExtensionPoint("entity:custom-fields", {
168
+ const { components, hasComponents } = defineExtensionPoint("seller:commissions", {
169
169
  description: "Commission fee fields in the seller details form",
170
170
  });
171
171
 
@@ -201,7 +201,7 @@ Used in modules that **provide** content to an extension point. Returns `add` an
201
201
  import { useExtensionPoint } from "@vc-shell/framework";
202
202
  import MyComponent from "./MyComponent.vue";
203
203
 
204
- const { add, remove } = useExtensionPoint("entity:custom-fields");
204
+ const { add, remove } = useExtensionPoint("seller:commissions");
205
205
 
206
206
  // Register a component
207
207
  add({
@@ -231,7 +231,7 @@ The `<ExtensionPoint>` component is the easiest way to render extensions in a te
231
231
 
232
232
  ```vue
233
233
  <template>
234
- <ExtensionPoint name="entity:custom-fields" />
234
+ <ExtensionPoint name="seller:commissions" />
235
235
  </template>
236
236
 
237
237
  <script setup lang="ts">
@@ -260,10 +260,10 @@ The component has three rendering modes, chosen automatically:
260
260
 
261
261
  ```vue
262
262
  <!-- Layout mode: separator + gap -->
263
- <ExtensionPoint name="entity:custom-fields" separator gap="1rem" wrapper-class="tw-p-4" />
263
+ <ExtensionPoint name="seller:commissions" separator gap="1rem" wrapper-class="tw-p-4" />
264
264
 
265
265
  <!-- Plain mode: no wrapper -->
266
- <ExtensionPoint name="entity:custom-fields" />
266
+ <ExtensionPoint name="seller:commissions" />
267
267
  ```
268
268
 
269
269
  ### Scoped Slots for Custom Rendering
@@ -334,7 +334,7 @@ Currently defined constants:
334
334
  | --------------------------------- | ------------------- | ---------------------------------- |
335
335
  | `ExtensionPoints.AUTH_AFTER_FORM` | `"auth:after-form"` | Login page, below the sign-in form |
336
336
 
337
- App-level extension point names (e.g., `"entity:custom-fields"`) are the app's responsibility to define. Consider creating a shared constants file in your application.
337
+ App-level extension point names (e.g., `"seller:commissions"`) are the app's responsibility to define. Consider creating a shared constants file in your application.
338
338
 
339
339
  ---
340
340
 
@@ -356,7 +356,7 @@ Scenario: You want to add commission fee fields to the Seller Details page, whic
356
356
 
357
357
  <ExtensionPoint
358
358
  v-if="sellerDetails?.id"
359
- name="entity:custom-fields"
359
+ name="seller:commissions"
360
360
  wrapper-class="tw-p-2"
361
361
  />
362
362
  </VcBlade>
@@ -368,13 +368,13 @@ Scenario: You want to add commission fee fields to the Seller Details page, whic
368
368
  ```typescript
369
369
  // commissions/index.ts
370
370
  import { defineAppModule, useExtensionPoint } from "@vc-shell/framework";
371
- import CustomFields from "./components/CustomFields.vue";
371
+ import CommissionFields from "./components/CommissionFields.vue";
372
372
  import en from "./locales/en.json";
373
373
 
374
- const { add } = useExtensionPoint("entity:custom-fields");
374
+ const { add } = useExtensionPoint("seller:commissions");
375
375
  add({
376
376
  id: "commission-fields",
377
- component: CustomFields,
377
+ component: CommissionFields,
378
378
  props: { editable: true },
379
379
  priority: 10,
380
380
  });
@@ -382,10 +382,10 @@ add({
382
382
  export default defineAppModule({ locales: { en } });
383
383
  ```
384
384
 
385
- **Step 3:** `CustomFields.vue` receives props and renders:
385
+ **Step 3:** `CommissionFields.vue` receives props and renders:
386
386
 
387
387
  ```vue
388
- <!-- commissions/components/CustomFields.vue -->
388
+ <!-- commissions/components/CommissionFields.vue -->
389
389
  <template>
390
390
  <div class="commission-fields">
391
391
  <h3>{{ $t("COMMISSIONS.TITLE") }}</h3>
@@ -449,14 +449,14 @@ The host renders them in order: ShippingInfo, PaymentInfo, OrderNotes.
449
449
  If you call `add()` with an `id` that already exists, the component is replaced:
450
450
 
451
451
  ```typescript
452
- const { add } = useExtensionPoint("entity:custom-fields");
452
+ const { add } = useExtensionPoint("seller:commissions");
453
453
 
454
454
  // Original registration (e.g., from another module or earlier in the code)
455
- add({ id: "commission-fields", component: OldCustomFields, priority: 10 });
455
+ add({ id: "commission-fields", component: OldCommissionFields, priority: 10 });
456
456
 
457
457
  // Override with a new component (same id)
458
- add({ id: "commission-fields", component: NewCustomFields, priority: 10 });
459
- // Only NewCustomFields is rendered
458
+ add({ id: "commission-fields", component: NewCommissionFields, priority: 10 });
459
+ // Only NewCommissionFields is rendered
460
460
  ```
461
461
 
462
462
  This is useful for overriding default behavior provided by a base module.
@@ -504,7 +504,7 @@ add({ id: "quick-action", component: QuickAction, meta: { zone: "toolbar" } });
504
504
  // WRONG -- typo: "seller:comissions" (single 'm')
505
505
  const { add } = useExtensionPoint("seller:comissions");
506
506
  add({ id: "my-fields", component: MyFields });
507
- // Component is registered but never rendered because the host declared "entity:custom-fields"
507
+ // Component is registered but never rendered because the host declared "seller:commissions"
508
508
  ```
509
509
 
510
510
  The dev-mode console warning (`Extension point "seller:comissions" is not declared`) will alert you to this. Always check the console in development.
@@ -514,7 +514,7 @@ The dev-mode console warning (`Extension point "seller:comissions" is not declar
514
514
  > ```typescript
515
515
  > // shared/extension-points.ts
516
516
  > export const EP = {
517
- > ENTITY_CUSTOM_FIELDS: "entity:custom-fields",
517
+ > SELLER_COMMISSIONS: "seller:commissions",
518
518
  > ORDER_SIDEBAR: "order:sidebar",
519
519
  > } as const;
520
520
  > ```
@@ -524,7 +524,7 @@ The dev-mode console warning (`Extension point "seller:comissions" is not declar
524
524
  ```vue
525
525
  <!-- WRONG -- ExtensionPoint is not imported -->
526
526
  <template>
527
- <ExtensionPoint name="entity:custom-fields" />
527
+ <ExtensionPoint name="seller:commissions" />
528
528
  </template>
529
529
 
530
530
  <script setup lang="ts">
@@ -546,7 +546,7 @@ add({ id: "my-component", component: ComponentB });
546
546
  Use globally unique IDs. A good convention is `module-name:component-name`:
547
547
 
548
548
  ```typescript
549
- add({ id: "module:custom-fields", component: CustomFields });
549
+ add({ id: "marketplace:commission-fields", component: CommissionFields });
550
550
  add({ id: "shipping:delivery-estimate", component: DeliveryEstimate });
551
551
  ```
552
552
 
@@ -668,9 +668,10 @@ These are used internally by `defineExtensionPoint` and `useExtensionPoint`. You
668
668
 
669
669
  ## Related
670
670
 
671
- | Resource | Path | Description |
672
- | ------------------------ | -------------------------------------------------- | --------------------------------------------- |
673
- | Modularity Plugin | `core/plugins/modularity/` | Module definition and registration |
674
- | Extension Point Store | `core/plugins/extension-points/store.ts` | Reactive registry implementation |
675
- | ExtensionPoint Component | `core/plugins/extension-points/ExtensionPoint.vue` | Declarative render component |
676
- | Types | `core/plugins/extension-points/types.ts` | `ExtensionComponent`, `ExtensionPointOptions` |
671
+ | Resource | Path | Description |
672
+ | ------------------------------ | -------------------------------------------------- | --------------------------------------------- |
673
+ | Modularity Plugin | `core/plugins/modularity/` | Module definition and registration |
674
+ | Extension Point Store | `core/plugins/extension-points/store.ts` | Reactive registry implementation |
675
+ | ExtensionPoint Component | `core/plugins/extension-points/ExtensionPoint.vue` | Declarative render component |
676
+ | Types | `core/plugins/extension-points/types.ts` | `ExtensionComponent`, `ExtensionPointOptions` |
677
+ | Seller Details (usage example) | `apps/vendor-portal/src/modules/seller-details/` | Real-world extension point host |
@@ -703,16 +703,16 @@ export default defineAppModule({});
703
703
  A module can extend another module's UI by using extension points (see the [Extension Points Plugin](../extension-points/extension-points.docs.md)):
704
704
 
705
705
  ```typescript
706
- // modules/entity-extensions/index.ts
706
+ // modules/marketplace-commissions/index.ts
707
707
  import { defineAppModule, useExtensionPoint } from "@vc-shell/framework";
708
- import CustomFields from "./components/CustomFields.vue";
708
+ import CommissionFields from "./components/CommissionFields.vue";
709
709
  import en from "./locales/en.json";
710
710
 
711
- // Register into the entity details extension point
712
- const { add } = useExtensionPoint("entity:custom-fields");
711
+ // Register into the seller details extension point
712
+ const { add } = useExtensionPoint("seller:commissions");
713
713
  add({
714
- id: "entity-extension",
715
- component: CustomFields,
714
+ id: "marketplace-commission",
715
+ component: CommissionFields,
716
716
  props: { showAdvanced: true },
717
717
  priority: 10,
718
718
  });
@@ -733,7 +733,7 @@ The host blade declares the extension point:
733
733
  <!-- Other modules can inject components here -->
734
734
  <ExtensionPoint
735
735
  v-if="sellerDetails?.id"
736
- name="entity:custom-fields"
736
+ name="seller:commissions"
737
737
  wrapper-class="tw-p-2"
738
738
  />
739
739
  </VcBlade>
@@ -46,7 +46,7 @@ This component has no props. All content is driven by the settings menu service
46
46
  A typical module registers all its settings entries during the module install phase:
47
47
 
48
48
  ```ts
49
- // my-module/index.ts
49
+ // vendor-portal-module/index.ts
50
50
  import { markRaw } from "vue";
51
51
  import { useSettingsMenu, ThemeSelector, LanguageSelector, ChangePasswordButton, LogoutButton } from "@vc-shell/framework";
52
52
 
@@ -10,12 +10,13 @@ The composables follow a PrimeVue-inspired pattern: each returns reactive state
10
10
 
11
11
  ### Data & State
12
12
 
13
- | Composable | Purpose |
14
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
15
- | `useDataTableState` | Persists column widths, order, and hidden IDs to localStorage/sessionStorage. Key format: `VC_DATATABLE_{KEY}`. Debounced auto-save (150ms) with restore-on-mount. |
16
- | `useTableColumns` | Column ordering, width tracking, alignment helpers, flex-column detection. Watches `visibleColumns` and appends new columns without dropping hidden ones. |
17
- | `useDataProcessing` | Client-side sort pipeline (single/multi) and row grouping. Skipped when `lazy: true` (server-side). |
18
- | `useTableContext` | Provides/injects table-level context for sub-components. |
13
+ | Composable | Purpose |
14
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
+ | `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. |
16
+ | `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. |
17
+ | `useColumnWidthEngine` | Pure functions for deterministic column width computation (see below). |
18
+ | `useDataProcessing` | Client-side sort pipeline (single/multi) and row grouping. Skipped when `lazy: true` (server-side). |
19
+ | `useTableContext` | Provides/injects table-level context for sub-components. |
19
20
 
20
21
  ### Sorting & Filtering
21
22
 
@@ -27,13 +28,13 @@ The composables follow a PrimeVue-inspired pattern: each returns reactive state
27
28
 
28
29
  ### Interaction
29
30
 
30
- | Composable | Purpose |
31
- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32
- | `useTableRowReorder` | Drag-and-drop row reordering with live-swap at 50% vertical threshold. Commits via `dragend` (always fires) with `drop` as preferred path. |
33
- | `useTableColumnsReorder` | Drag-and-drop column reordering with 50% horizontal threshold. Returns `getReorderHeadProps()` for easy binding. |
34
- | `useTableColumnsResize` | Two-column resize: dragging grows left column and shrinks right neighbor. DOM-based updates during drag for smooth 60fps; commits to reactive state on mouseup. |
35
- | `useTableSelectionV2` | Row selection: single, multiple (checkbox), and row-click modes. Emits `RowSelectEvent` / `RowSelectAllEvent`. |
36
- | `useTableSwipe` | Mobile swipe context (provide/inject). Tracks which row has an active swipe action. |
31
+ | Composable | Purpose |
32
+ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
+ | `useTableRowReorder` | Drag-and-drop row reordering with live-swap at 50% vertical threshold. Commits via `dragend` (always fires) with `drop` as preferred path. |
34
+ | `useTableColumnsReorder` | Drag-and-drop column reordering with 50% horizontal threshold. Returns `getReorderHeadProps()` for easy binding. |
35
+ | `useTableColumnsResize` | Weight-based resize: dragging adjusts the weights of the dragged column and its right neighbor without touching other columns. DOM-based px updates during drag for smooth 60fps; commits new weights to `columnState` on mouseup. No `ResizeObserver` scaling. |
36
+ | `useTableSelectionV2` | Row selection: single, multiple (checkbox), and row-click modes. Emits `RowSelectEvent` / `RowSelectAllEvent`. |
37
+ | `useTableSwipe` | Mobile swipe context (provide/inject). Tracks which row has an active swipe action. |
37
38
 
38
39
  ### Editing
39
40
 
@@ -53,6 +54,68 @@ The composables follow a PrimeVue-inspired pattern: each returns reactive state
53
54
  | `useTableRowGrouping` | Groups rows by field and inserts group header rows. |
54
55
  | `useMobileCardLayout` | Mobile card view layout logic for responsive table display. |
55
56
 
57
+ ## useColumnWidthEngine
58
+
59
+ `useColumnWidthEngine` is a collection of **pure functions** (no reactive state) for deterministic column width computation. `useTableColumns` calls these internally on every render pass and on container resize.
60
+
61
+ ### Core functions
62
+
63
+ #### `computeColumnWidths(input: EngineInput): EngineOutput`
64
+
65
+ The central engine. Distributes `availableWidth` among visible columns according to their current weights, enforcing `minWidth` / `maxWidth` constraints.
66
+
67
+ ```ts
68
+ interface EngineInput {
69
+ visibleIds: string[]; // Ordered list of visible column IDs
70
+ specs: Record<string, ColumnSpec>; // Per-column: weight, minWidth, maxWidth
71
+ availableWidth: number; // Container width in px
72
+ fitMode: "gap" | "fit"; // "gap" leaves filler space, "fit" fills width
73
+ }
74
+
75
+ interface EngineOutput {
76
+ widths: Record<string, number>; // Computed px width per column ID
77
+ totalWidth: number; // Sum of all computed widths
78
+ overflow: boolean; // true when sum(minWidth) > availableWidth
79
+ }
80
+ ```
81
+
82
+ In crisis (`overflow: true`), each column receives its `minWidth` regardless of weight, and a console warning is emitted.
83
+
84
+ #### `parseColumnWidth(value: string | number | undefined, availableWidth: number): ParsedWidth`
85
+
86
+ Parses a `VcColumn` `width` prop into a concrete pixel value.
87
+
88
+ ```ts
89
+ type ParsedWidth =
90
+ | { type: "px"; value: number } // "200", 200, "200px"
91
+ | { type: "pct"; value: number } // "20%" → 0.2 * availableWidth
92
+ | { type: "auto"; value: undefined }; // undefined, "auto"
93
+ ```
94
+
95
+ #### `buildInitialWeights(parsed: ParsedWidth[], availableWidth: number): Record<string, number>`
96
+
97
+ Converts an array of `ParsedWidth` values (one per column) into initial weights. Auto columns receive an equal share of the space not claimed by px/% columns.
98
+
99
+ ```ts
100
+ // Example: three columns — 200px, 20%, auto — with 800px available
101
+ // px-column → weight 200
102
+ // pct-column → weight 160 (20% of 800)
103
+ // auto-column→ weight 440 (800 - 200 - 160)
104
+ ```
105
+
106
+ #### `normalizeWeights(specs: Record<string, ColumnSpec>, visibleIds: string[]): void`
107
+
108
+ Mutates `specs` in place so that the weights of `visibleIds` sum to 1.0. Called before a `"fit"` mode computation pass.
109
+
110
+ ### When weights update
111
+
112
+ | User action | Weight change |
113
+ | --------------------------- | --------------------------------------------------------------------------------------- |
114
+ | Column resize (drag border) | Dragged column and right neighbor exchange weight proportionally |
115
+ | Column show/hide | Hidden column's weight is preserved; shown column uses saved or initial weight |
116
+ | Reset columns | All weights rebuilt from declarative `width` props |
117
+ | Container resize | Weights unchanged; engine recomputes px widths from same weights × new `availableWidth` |
118
+
56
119
  ## Usage
57
120
 
58
121
  These composables are consumed internally by VcDataTable. If you need to interact with them, use VcDataTable's props and events:
@@ -79,10 +142,12 @@ register({
79
142
 
80
143
  ## Tips
81
144
 
82
- - `useTableColumns` never removes entries from `columnWidths` -- hidden columns preserve their width/order for when they reappear.
145
+ - `useTableColumns` never removes entries from `columnState` hidden columns preserve their weight and order for when they reappear. Use `engineOutput` (not `columnState` directly) to read computed pixel widths for rendering.
146
+ - Call `recompute()` (returned by `useTableColumns`) whenever the container width changes to force the engine to redistribute widths without altering weights.
147
+ - `useDataTableState` stores the v2 schema (weights, order, hidden/shown IDs). It automatically migrates v1 state (pixel-based) on the first restore. Guard against save-during-restore loops with the `isRestoring` flag.
148
+ - `useColumnWidthEngine` functions are pure — pass immutable copies of `specs` when testing or previewing layouts without committing to state.
83
149
  - `useTableRowReorder`: `event.preventDefault()` in `dragover` MUST be called on every event or `drop` never fires.
84
- - `useTableColumnsResize` pins all columns to fixed widths during drag to prevent flex redistribution, then unpins on mouseup.
85
- - `useDataTableState` guards against save-during-restore loops with an `isRestoring` flag.
150
+ - `useTableColumnsResize` applies DOM-level px changes during drag for 60fps performance, then commits final weights to `columnState` on mouseup. No `ResizeObserver` scaling is involved.
86
151
  - `useVirtualScroll` requires a fixed `itemSize` (row height in pixels) for accurate positioning.
87
152
 
88
153
  ## Related
@@ -551,6 +551,40 @@ Columns are reorderable by default. Drag a column header to a new position. Disa
551
551
  </VcDataTable>
552
552
  ```
553
553
 
554
+ ### Column Width Model
555
+
556
+ VcDataTable uses a **weight-based engine** to compute exact pixel widths for every column deterministically, based on the container's available width.
557
+
558
+ **How it works:**
559
+
560
+ 1. Developer declares initial widths via the `VcColumn` `width` prop (px, %, or omitted for auto).
561
+ 2. At runtime, these declarations become proportional weights. The engine converts weights to exact pixel values by distributing `availableWidth` proportionally.
562
+ 3. When a user resizes a column, only the weights change — the engine recomputes all pixel widths from the new weights on the next render.
563
+ 4. Clicking **Reset columns** returns all columns to their declarative hints.
564
+
565
+ **`fitMode` prop** controls what happens to leftover space:
566
+
567
+ | Value | Behavior |
568
+ | ----------------- | ----------------------------------------------------------------------------- |
569
+ | `"gap"` (default) | A filler pseudo-element absorbs unused space at the right end. |
570
+ | `"fit"` | All column weights are normalized so columns fill the entire container width. |
571
+
572
+ **Width prop contract:**
573
+
574
+ | Declaration | Meaning |
575
+ | ------------------------------- | ------------------------------------------------------------ |
576
+ | `width="200"` or `:width="200"` | Initial 200 px hint |
577
+ | `width="20%"` | Initial hint based on 20% of available width |
578
+ | `width` omitted | Auto — splits remaining space equally among all auto columns |
579
+
580
+ After initialization the column lives in the weight model. Container resizes recompute px values without changing weights.
581
+
582
+ **`minWidth` / `maxWidth`:**
583
+
584
+ - `minWidth` and `maxWidth` are enforced by the engine on every computation pass.
585
+ - Default `minWidth` is 40 px when not specified.
586
+ - In crisis (sum of all `minWidth` values exceeds available width), the engine squeezes columns below their minimums and emits a console warning rather than breaking layout.
587
+
554
588
  ### Column Switcher
555
589
 
556
590
  A built-in panel lets users show/hide columns. Enabled by default when `column-switcher` is truthy.
@@ -992,6 +1026,10 @@ Persist column widths, column order, hidden columns, sort, and filters across pa
992
1026
 
993
1027
  **Storage key format:** `VC_DATATABLE_PRODUCT-LIST` (uppercased `state-key`).
994
1028
 
1029
+ **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.
1030
+
1031
+ 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.
1032
+
995
1033
  Use sessionStorage instead of localStorage:
996
1034
 
997
1035
  ```vue
@@ -1534,9 +1572,9 @@ function onRowRemove(event: { data: Product; index: number; cancel: () => void }
1534
1572
 
1535
1573
  ## Recipes
1536
1574
 
1537
- ### Recipe 1: Entity List Blade
1575
+ ### Recipe 1: Products List Blade
1538
1576
 
1539
- A typical list blade with search, pagination, row actions, and empty states.
1577
+ A typical list blade with search, pagination, row actions, and empty states -- modeled after real vendor-portal usage.
1540
1578
 
1541
1579
  ```vue
1542
1580
  <template>
@@ -11,23 +11,23 @@ A responsive multi-image gallery with drag-and-drop reorder, file upload, lightb
11
11
 
12
12
  ## Props
13
13
 
14
- | Prop | Type | Default | Description |
15
- | --------------- | --------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
16
- | `layout` | `"filmstrip" \| "grid"` | `"filmstrip"` | Layout mode — filmstrip shows a single scrollable row with expand/collapse; grid shows the classic multi-row auto-fill layout. |
17
- | `label` | `string` | `undefined` | Label text displayed in the gallery header. |
18
- | `required` | `boolean` | `false` | Shows a required indicator (`*`) on the label. |
19
- | `images` | `ICommonAsset[]` | `[]` | Array of image assets to display. |
20
- | `disabled` | `boolean` | `false` | Disables all interactive actions. |
21
- | `multiple` | `boolean` | `false` | Allow selecting multiple files in upload dialog. |
22
- | `loading` | `boolean` | `false` | Shows a loading overlay with spinner on the gallery. |
23
- | `itemActions` | `{ preview?: boolean; edit?: boolean; remove?: boolean }` | `{ preview: true, edit: true, remove: true }` | Per-tile action visibility. |
24
- | `rules` | `IValidationRules` | `undefined` | Validation rules for uploaded files. |
25
- | `name` | `string` | `"Gallery"` | Field name for validation messages. |
26
- | `accept` | `string` | `undefined` | Accepted file extensions. |
27
- | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Tile size preset. Sizes are smaller on mobile. |
28
- | `gap` | `number` | `8` | Gap between tiles in pixels. |
29
- | `imagefit` | `"contain" \| "cover"` | `"contain"` | How images fit within tiles. |
30
- | `thumbnailSize` | `ThumbnailSize` | auto from `size` | Thumbnail size for tile images. Auto-mapped: sm→128x128, md→216x216, lg→348x348. Preview thumbnails use 64x64. |
14
+ | Prop | Type | Default | Description |
15
+ | --------------- | --------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | `layout` | `"filmstrip" \| "grid"` | `"filmstrip"` | Layout mode — filmstrip shows a single scrollable row with expand/collapse; grid shows the classic multi-row auto-fill layout. |
17
+ | `label` | `string` | `undefined` | Label text displayed in the gallery header. |
18
+ | `required` | `boolean` | `false` | Shows a required indicator (`*`) on the label. |
19
+ | `images` | `ICommonAsset[]` | `[]` | Array of image assets to display. |
20
+ | `disabled` | `boolean` | `false` | Disables all interactive actions. |
21
+ | `multiple` | `boolean` | `false` | Allow selecting multiple files in upload dialog. |
22
+ | `loading` | `boolean` | `false` | Shows a loading overlay with spinner on the gallery. |
23
+ | `itemActions` | `{ preview?: boolean; edit?: boolean; remove?: boolean }` | `{ preview: true, edit: true, remove: true }` | Per-tile action visibility. |
24
+ | `rules` | `IValidationRules` | `undefined` | Validation rules for uploaded files. |
25
+ | `name` | `string` | `"Gallery"` | Field name for validation messages. |
26
+ | `accept` | `string` | `"image/*"` | Accepted file MIME types / extensions. Gallery is image-only by default — non-image files dropped from the OS are filtered out. Override (e.g. `"image/png,image/jpeg"`) to narrow further. |
27
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Tile size preset. Sizes are smaller on mobile. |
28
+ | `gap` | `number` | `8` | Gap between tiles in pixels. |
29
+ | `imagefit` | `"contain" \| "cover"` | `"contain"` | How images fit within tiles. |
30
+ | `thumbnailSize` | `ThumbnailSize` | auto from `size` | Thumbnail size for tile images. Auto-mapped: sm→128x128, md→216x216, lg→348x348. Preview thumbnails use 64x64. |
31
31
 
32
32
  ## Events
33
33