cabloy 5.1.158 → 5.1.160

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.
Files changed (120) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-contract-loop/SKILL.md +24 -7
  3. package/.claude/skills/cabloy-contract-loop/evals/evals.json +12 -0
  4. package/.claude/skills/cabloy-contract-loop/references/contract-loop-map.md +28 -0
  5. package/.claude/skills/cabloy-contract-loop/references/verification-checklist.md +19 -2
  6. package/.claude/skills/cabloy-resource-field-update/SKILL.md +18 -0
  7. package/.claude/skills/cabloy-zova-source-reading/SKILL.md +14 -0
  8. package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +10 -0
  9. package/.github/workflows/docs-pages.yml +18 -2
  10. package/CHANGELOG.md +31 -0
  11. package/package.json +2 -1
  12. package/repo-docs/.vitepress/config.mjs +45 -4
  13. package/repo-docs/.vitepress/theme/components/GitHubRepositoriesNav.vue +255 -0
  14. package/repo-docs/.vitepress/theme/custom.css +21 -0
  15. package/repo-docs/.vitepress/theme/index.js +7 -1
  16. package/repo-docs/backend/department-management.md +98 -0
  17. package/repo-docs/backend/menu-authorization.md +124 -0
  18. package/repo-docs/backend/rbac-authorization.md +117 -0
  19. package/repo-docs/backend/resource-field-update.md +49 -2
  20. package/repo-docs/backend/role-management.md +92 -0
  21. package/repo-docs/backend/shared-rbac-architecture.md +339 -0
  22. package/repo-docs/backend/user-management.md +92 -0
  23. package/repo-docs/blogs/index.md +2 -0
  24. package/repo-docs/frontend/form-layout-guide.md +6 -6
  25. package/repo-docs/frontend/model-resource-owner-pattern.md +2 -0
  26. package/repo-docs/frontend/page-guide.md +12 -0
  27. package/repo-docs/frontend/page-meta-guide.md +27 -0
  28. package/repo-docs/frontend/permission-formscene-action-visibility-guide.md +5 -3
  29. package/repo-docs/frontend/ssr-architecture-overview.md +4 -0
  30. package/repo-docs/frontend/ssr-build-deploy-guide.md +1 -1
  31. package/repo-docs/frontend/table-action-visibility-permission-flow-guide.md +2 -0
  32. package/repo-docs/frontend/table-guide.md +75 -22
  33. package/repo-docs/frontend/table-resource-crud-cookbook.md +25 -4
  34. package/repo-docs/frontend/zova-form-source-reading-map.md +2 -2
  35. package/repo-docs/frontend/zova-form-under-the-hood.md +1 -1
  36. package/repo-docs/frontend/zova-reactivity-under-the-hood.md +65 -9
  37. package/repo-docs/frontend/zova-table-source-reading-map.md +54 -19
  38. package/repo-docs/frontend/zova-table-under-the-hood.md +70 -39
  39. package/repo-docs/fullstack/contract-loop-playbook.md +2 -0
  40. package/repo-docs/fullstack/ssr-site-and-flavor-setup.md +198 -0
  41. package/repo-docs/fullstack/vona-zova-integration.md +2 -0
  42. package/repo-e2e/docs/playwright.config.ts +37 -0
  43. package/repo-e2e/docs/specs/blogs-index.spec.ts +180 -0
  44. package/repo-e2e/docs/specs/github-repositories-nav.spec.ts +69 -0
  45. package/repo-e2e/docs/test-results/.last-run.json +4 -0
  46. package/repo-e2e/specs/cabloy-basic.spec.ts +136 -2
  47. package/test-results/.last-run.json +20 -2
  48. package/test-results/a-commerce-ATP-ADDR-01-aut-11dea-ss-through-Web-self-service/error-context.md +238 -0
  49. package/test-results/a-commerce-ATP-SPC-01-Coup-ef380-mantic-Admin-field-controls/error-context.md +240 -0
  50. package/test-results/a-commerce-ATP-SPC-02-Cate-9f120-on-and-publication-controls/error-context.md +238 -0
  51. package/test-results/a-commerce-ATP-SPC-02-Prod-5f92a-on-and-publication-controls/error-context.md +238 -0
  52. package/test-results/a-commerce-ATP-SPC-02-SKU--2e61e-ency-and-lifecycle-controls/error-context.md +238 -0
  53. package/test-results/a-commerce-ATP-SPC-04-Stoc-fa895--readonly-and-mutation-free/error-context.md +235 -0
  54. package/test-results/a-commerce-ATP-SPC-05-syst-fb406-e-without-mutation-controls/error-context.md +238 -0
  55. package/test-results/a-commerce-Commerce-sessio-4e963-ie-selects-raw-server-theme/error-context.md +226 -0
  56. package/test-results/a-commerce-Commerce-theme--062fe--without-hydration-mismatch/error-context.md +229 -0
  57. package/test-results/a-commerce-PayPal-browser--8672c-t-or-open-an-awaiting-order/error-context.md +238 -0
  58. package/test-results/a-commerce-Payment-callbac-f56c2--reconciles-after-hydration/error-context.md +238 -0
  59. package/test-results/a-commerce-Payment-cancell-3fc74-ified-provider-confirmation/error-context.md +238 -0
  60. package/test-results/a-commerce-Phase-50-60-aut-71d61--observes-operator-shipment/error-context.md +238 -0
  61. package/test-results/a-commerce-Phase-60-custom-407cd-ecutes-a-whole-order-refund/error-context.md +238 -0
  62. package/test-results/cabloy-basic-ATP-BASIC-TAB-e7253-and-pins-configured-columns/error-context.md +240 -0
  63. package/test-results/markdown-ATP-SPC-02-Produc-9d5c2-link-toolbar-edits-Markdown/error-context.md +211 -0
  64. package/test-results/markdown-ATP-SPC-02-Produc-9d6ea-t-editor-and-saves-Markdown/error-context.md +238 -0
  65. package/vona/packages-cli/cli/package.json +1 -1
  66. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/entity/{{resourceName}}.tsx_ +1 -1
  67. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/entity/{{resourceName}}.tsx_ +1 -1
  68. package/vona/packages-cli/cli-set-api/package.json +1 -1
  69. package/vona/packages-vona/vona/package.json +1 -1
  70. package/vona/pnpm-lock.yaml +79 -52
  71. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
  72. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
  73. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productCreate.tsx +2 -0
  74. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productUpdate.tsx +2 -0
  75. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +2 -0
  76. package/vona/src/suite/a-training/modules/training-record/src/dto/recordCreate.tsx +1 -0
  77. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectReq.tsx +3 -7
  78. package/vona/src/suite/a-training/modules/training-record/src/dto/recordUpdate.tsx +1 -0
  79. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +1 -0
  80. package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +8 -0
  81. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +51 -0
  82. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +3 -0
  83. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +3 -0
  84. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +1 -0
  85. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +1 -0
  86. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +1 -0
  87. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +1 -0
  88. package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +10 -1
  89. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +99 -1
  90. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  91. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  92. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  93. package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
  94. package/vona/src/suite-vendor/a-vona/modules/a-web/src/bean/pipe.filter.ts +85 -57
  95. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  96. package/zova/packages-cli/cli/package.json +2 -2
  97. package/zova/packages-cli/cli-set-front/cli/templates/rest/render.ts +2 -0
  98. package/zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts +13 -3
  99. package/zova/packages-cli/cli-set-front/package.json +1 -1
  100. package/zova/packages-zova/zova/package.json +2 -2
  101. package/zova/pnpm-lock.yaml +16 -16
  102. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +31 -11
  103. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +3 -0
  104. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +1 -1
  105. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +68 -8
  106. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  107. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +4 -0
  108. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  109. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  110. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +0 -6
  111. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts +16 -9
  112. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  113. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +4 -0
  114. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +13 -2
  115. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/schema.ts +1 -0
  116. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  117. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +50 -0
  118. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +60 -7
  119. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/table.ts +6 -4
  120. package/zova/src/suite-vendor/a-zova/package.json +4 -4
@@ -58,13 +58,14 @@ This page is that bridge.
58
58
 
59
59
  For a typical Zova table, the shortest accurate model is:
60
60
 
61
- 1. the `ZTable` wrapper creates a table controller bean through the normal Zova controller path
62
- 2. the table controller loads table-scene schema properties from the row schema
63
- 3. the controller builds table metadata with visible properties and per-column render functions
64
- 4. the controller creates TanStack table options through Zova’s `$useTable(...)` wrapper
65
- 5. each cell render resolves either to text fallback, a general JSX render target, or a `tableCell` bean
66
- 6. the cell runtime evaluates JSX/CEL props with table-aware column and cell scope
67
- 7. resource pages feed schema, data, permissions, and page scope into the same table runtime through `basic-page:blockTable`
61
+ 1. backend schema helpers such as `ZovaRender.column(...)` attach table-column metadata to the field contract
62
+ 2. the `ZTable` wrapper creates a table controller bean through the normal Zova controller path
63
+ 3. the table controller loads effective table-scene schema properties from the row schema
64
+ 4. the controller builds table metadata with visible properties and per-column render functions
65
+ 5. the controller creates TanStack table options through Zova’s `$useTable(...)` wrapper
66
+ 6. each cell render resolves either to text fallback, a general JSX render target, or a `tableCell` bean
67
+ 7. the cell runtime evaluates JSX/CEL props with table-aware column and cell scope
68
+ 8. resource pages feed schema, data, permissions, and page scope into the same table runtime through `basic-page:blockTable`
68
69
 
69
70
  That is why Zova Table is not only a thin wrapper around TanStack Table. The business-facing runtime surface is still Zova-native.
70
71
 
@@ -98,25 +99,27 @@ These three files already show the core architecture:
98
99
 
99
100
  When you want to trace the full mechanism, read these files in order:
100
101
 
101
- 1. `zova/src/suite-vendor/a-zova/modules/a-table/src/.metadata/component/table.ts`
102
- 2. `zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx`
103
- 3. `zova/src/suite-vendor/a-zova/modules/a-table/src/lib/beanControllerTableBase.ts`
104
- 4. `zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx`
105
- 5. `zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts`
106
- 6. `zova/src/suite-vendor/a-zova/modules/a-table/src/types/tableCell.ts`
107
- 7. `zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx`
108
- 8. `zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx`
102
+ 1. `zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts`
103
+ 2. `zova/src/suite-vendor/a-zova/modules/a-table/src/.metadata/component/table.ts`
104
+ 3. `zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx`
105
+ 4. `zova/src/suite-vendor/a-zova/modules/a-table/src/lib/beanControllerTableBase.ts`
106
+ 5. `zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx`
107
+ 6. `zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts`
108
+ 7. `zova/src/suite-vendor/a-zova/modules/a-table/src/types/tableCell.ts`
109
+ 8. `zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx`
110
+ 9. `zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx`
109
111
 
110
112
  A compact role map is:
111
113
 
114
+ - `cli/templates/rest/rest.ts` shows that `ZovaRender.column(...)` stores its options under `rest.table`
112
115
  - `table.ts` shows how the public wrapper enters `useController(...)`
113
- - `component/table/controller.tsx` owns schema properties, metadata refresh, TanStack bridge, and cell rendering
116
+ - `component/table/controller.tsx` owns schema properties, metadata refresh, TanStack bridge, pinning, sorting eligibility, and cell rendering
114
117
  - `beanControllerTableBase.ts` shows the Zova wrapper around `useVueTable(...)`
115
- - `component/table/render.tsx` shows the default table DOM render path through `FlexRender`
116
- - `schema.ts` shows how table-scene schema properties are loaded and ordered
118
+ - `component/table/render.tsx` shows the default table DOM render path, layout metadata, sortable headers, and `FlexRender`
119
+ - `schema.ts` shows how top-level and table-scene metadata are merged and ordered
117
120
  - `types/tableCell.ts` shows the `tableCell` scene contract
118
- - `blockTable/controller.tsx` shows how page blocks feed `data`, `schema`, and `tableScope` into `ZTable`
119
- - `blockPage/controller.tsx` shows where resource data, permissions, and page scope come from
121
+ - `blockTable/controller.tsx` shows how Basic page blocks feed data, schemas, scope, and sorting state into `ZTable`
122
+ - `blockPage/controller.tsx` shows where resource data, permissions, page scope, and backend `orders` mapping come from
120
123
 
121
124
  ## Step-by-step runtime path
122
125
 
@@ -205,16 +208,19 @@ zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts
205
208
 
206
209
  The important runtime path is:
207
210
 
208
- 1. the table receives `schema`
209
- 2. `_createProperties()` computes `this.$sdk.loadSchemaProperties(this.schema, 'table')`
210
- 3. `loadSchemaProperties(...)` resolves `$ref`, applies `rest.table` overlays, and sorts by `rest.order`
211
- 4. `_createTableMeta()` iterates those properties and decides visibility and render behavior
212
- 5. `_createColumnsMiddle()` converts the surviving properties into TanStack column definitions
211
+ 1. `ZovaRender.column(options)` stores the field's column options under `rest.table`
212
+ 2. the table receives `schema`
213
+ 3. `_createProperties()` computes `this.$sdk.loadSchemaProperties(this.schema, 'table')`
214
+ 4. `loadSchemaProperties(...)` resolves `$ref`, merges shared `rest` metadata with the `rest.table` overlay, and sorts by effective `rest.order`
215
+ 5. `_createTableMeta()` iterates those properties and decides visibility and render behavior
216
+ 6. `_createColumnsMiddle()` converts the surviving properties into TanStack column definitions
217
+
218
+ `ZovaRender.column(...)` is a shared Zova API available in both Cabloy Basic and Cabloy Start. It supplies metadata only; it neither registers a frontend column nor selects a cell renderer. `ZovaRender.cell(...)` supplies the render target, and `tableCell` beans implement reusable cell behavior.
213
219
 
214
220
  A practical reading takeaway is:
215
221
 
216
222
  - **schema is not only validation truth**
217
- - **schema also drives table order, visibility, and cell render metadata**
223
+ - **schema also drives table order, physical-column behavior, visibility, and cell render metadata**
218
224
 
219
225
  ## 5. How table metadata is built
220
226
 
@@ -257,6 +263,21 @@ A practical reading takeaway is:
257
263
  - **TanStack owns the row-model mechanics**
258
264
  - **the controller still owns which data and columns TanStack sees**
259
265
 
266
+ ### Column metadata becomes TanStack and DOM behavior
267
+
268
+ When `_createColumnsMiddle()` creates each surviving property column, it maps effective table metadata as follows:
269
+
270
+ | Metadata | Controller and render behavior |
271
+ | -------------------- | ---------------------------------------------------------------------------------------------------- |
272
+ | `rest.order` | Orders properties during schema-property loading. |
273
+ | `rest.width` | Becomes the TanStack `size`; render beans apply pixel `width` and `min-width`. |
274
+ | `rest.align` | Remains in column metadata; render beans apply header/cell text alignment. |
275
+ | `rest.fixed` | Builds left/right TanStack pinning arrays; render beans use sticky positions and calculated offsets. |
276
+ | `rest.enableSorting` | Enables sorting only when the field's key or aliases are also present in `schemaOrder`. |
277
+ | `rest.sortDescFirst` | Becomes TanStack's first-toggle direction. |
278
+
279
+ The sorting gate is intentional: metadata can request sorting, but the order schema remains the contract that authorizes the field. The standard resource-page table is controlled and `manualSorting`; it does not locally reorder the fetched data.
280
+
260
281
  ## 7. Column and cell render context are explicitly separated
261
282
 
262
283
  The controller creates two related but distinct runtime contexts.
@@ -392,12 +413,12 @@ That contract defines:
392
413
  The decorator itself is:
393
414
 
394
415
  ```typescript
395
- createBeanDecorator('tableCell', 'sys', true, options);
416
+ createBeanDecorator('tableCell', 'app', true, options);
396
417
  ```
397
418
 
398
- That means `tableCell` is not only a naming convention. It is a frontend bean scene with:
419
+ That means `tableCell` is not only a naming convention. It is an app-scoped frontend bean scene with:
399
420
 
400
- - system-scoped resolution behavior
421
+ - app-scoped reusable bean resolution
401
422
  - scene-level typing
402
423
  - CLI boilerplate support
403
424
  - metadata-driven resource identity
@@ -437,9 +458,12 @@ The default render bean lives in:
437
458
  zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx
438
459
  ```
439
460
 
440
- It does two important jobs:
461
+ It does more than delegate vnode creation:
441
462
 
442
463
  - render the outer table markup with `<table class="table">`
464
+ - apply effective column alignment, pixel width/minimum width, and fixed-column sticky positioning
465
+ - calculate and apply left/right pinned-column offsets
466
+ - render accessible sortable header buttons, `aria-sort`, and sort-state indicators when the controller exposes a sortable column
443
467
  - delegate header and cell vnode creation to TanStack `FlexRender`
444
468
 
445
469
  If `slotDefault` is supplied, the render bean yields to that slot instead of the built-in table DOM.
@@ -470,7 +494,8 @@ zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controll
470
494
  - loads `ModelResource`
471
495
  - creates page-level JSX/CEL environment
472
496
  - computes resource query state
473
- - exposes `data`, `schemaRow`, and `permissions`
497
+ - exposes `data`, `schemaRow`, `schemaOrder`, and `permissions`
498
+ - owns controlled table sorting and converts its single current sorting entry into backend `orders`
474
499
  - refreshes table metadata when permissions change
475
500
 
476
501
  ### Table block path
@@ -480,6 +505,8 @@ zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controll
480
505
  - renders `ZTable`
481
506
  - passes `data={$$page.data}`
482
507
  - passes `schema={$$page.schemaRow}`
508
+ - passes `schemaOrder={$$page.schemaOrder}`
509
+ - passes controlled `sorting` and `onSortingChange`
483
510
  - passes `tableScope={$$page.jsxCelScope}`
484
511
  - captures `controllerRef` and stores `tableRef` back onto the page controller
485
512
 
@@ -487,18 +514,22 @@ This is one of the most important integration facts about the module.
487
514
 
488
515
  The resource page does not manually rebuild the table runtime. It feeds page-owned resource state into the same reusable table controller.
489
516
 
517
+ This section uses Cabloy Basic `basic-page` paths as the concrete resource-page specimen. The core `ZovaRender.column(...)` metadata contract and the base Zova Table controller behavior are shared by Cabloy Basic and Cabloy Start; resolve Start-specific page modules and renderer keys from the active Start repository.
518
+
490
519
  ## 14. Compact call-flow sketch
491
520
 
492
521
  When in doubt, use this short call flow:
493
522
 
494
- 1. `ZTable` wrapper enters the normal Zova controller path
495
- 2. `ControllerTable.__init__()` creates CEL/JSX support and schema-driven properties
496
- 3. `refreshMeta()` computes visible table properties and per-column render functions
497
- 4. `_createTable()` creates the TanStack bridge through `$useTable(...)`
498
- 5. `RenderTable` renders headers and rows through `FlexRender`
499
- 6. each cell render resolves to text fallback, a general render target, or a `tableCell` bean
500
- 7. `tableCell` beans receive controller-prepared options, scope, and `next()`
501
- 8. resource pages prepare `data`, `schemaRow`, `permissions`, and `tableScope` before entering the same runtime
523
+ 1. `ZovaRender.column(...)` contributes `rest.table` metadata to the backend/OpenAPI field contract
524
+ 2. `ZTable` wrapper enters the normal Zova controller path
525
+ 3. `ControllerTable.__init__()` creates CEL/JSX support and schema-driven properties
526
+ 4. `refreshMeta()` computes visible table properties, column pinning, and per-column render functions
527
+ 5. `_createTable()` creates the TanStack bridge through `$useTable(...)`
528
+ 6. `RenderTable` applies column metadata and renders headers and rows through `FlexRender`
529
+ 7. each cell render resolves to text fallback, a general render target, or a `tableCell` bean
530
+ 8. `tableCell` beans receive controller-prepared options, scope, and `next()`
531
+ 9. resource pages prepare `data`, `schemaRow`, `schemaOrder`, sorting state, permissions, and `tableScope` before entering the same runtime
532
+ 10. a resource-page sort becomes backend `orders`; TanStack does not locally sort the fetched page
502
533
 
503
534
  That is the shortest end-to-end explanation of how the module cooperates.
504
535
 
@@ -146,6 +146,8 @@ Then use the **reverse chain**:
146
146
 
147
147
  See [Frontend Metadata Back to Backend](/fullstack/frontend-metadata-to-backend) for the end-to-end reverse-chain bridge from frontend-owned truth to backend-visible shared handoff.
148
148
 
149
+ When the frontend change creates a new independently mounted SSR site rather than refreshing an existing consumer, use [Independent SSR Site and Flavor Setup](/fullstack/ssr-site-and-flavor-setup). The new Vona consumer needs a matched flavor, paired SSR/REST build wrapper, site registration, and dispatch/hydration proof.
150
+
149
151
  ### 3. Do generated artifacts look stale?
150
152
 
151
153
  Examples:
@@ -0,0 +1,198 @@
1
+ # Independent SSR Site and Flavor Setup
2
+
3
+ Use this guide when adding a new deployable Zova SSR surface that Vona must dispatch and serve. It covers a new **site + flavor** pair, not merely a page or route inside an existing site.
4
+
5
+ The durable outcome is one aligned identity tuple:
6
+
7
+ ```text
8
+ Zova flavor
9
+ ↕
10
+ flavor env/config + Zova SSR/REST scripts
11
+ ↕
12
+ root paired build wrapper
13
+ ↕
14
+ SSR release directory + generated REST package
15
+ ↕
16
+ Vona @SsrSite registration
17
+ ```
18
+
19
+ Do not substitute identifiers from an existing Admin, Web, or business site. Select and verify every member of the tuple together.
20
+
21
+ ## Before you start
22
+
23
+ ### Detect the active edition
24
+
25
+ Read the repository marker first:
26
+
27
+ - `__CABLOY_BASIC__` → use the current Basic scripts, flavors, UI baselines, site modules, and paths.
28
+ - `__CABLOY_START__` → inspect the active Start repository before naming a flavor, wrapper, generated package, or site module.
29
+ - both markers → stop: the checkout is ambiguous.
30
+
31
+ The framework model is shared, but the exact flavor names, site baselines, UI layer, assets, scripts, and generated output paths can differ by edition. See [Edition Detection](/editions/detection) and [Edition Collaboration Differences](/fullstack/edition-collaboration-differences).
32
+
33
+ ### Confirm that a new site is needed
34
+
35
+ | Change | Normal boundary |
36
+ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
37
+ | New page or route inside an existing SSR site | Page, route, model, and existing site verification; normally no new flavor or `@SsrSite`. |
38
+ | Frontend metadata/resource handoff to an existing backend consumer | Reverse contract loop; rebuild every affected existing flavor pair, then run `npm run deps:vona`. |
39
+ | New deployable URL mount, SSR admission policy, assets, or independent frontend composition | New independent flavor and Vona SSR site; continue with this guide. |
40
+
41
+ A new site is an ownership decision, not a convenient alias for an existing Admin or Web surface.
42
+
43
+ ## Define the identity tuple
44
+
45
+ Choose these values before editing source. Keep their spelling and ownership consistent:
46
+
47
+ | Identity | Role |
48
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------- |
49
+ | `<site-flavor>` | Exact Zova flavor identifier used in env/config, lower-level scripts, build output, and generated REST package. |
50
+ | `<site-id>` | Vona SSR site identity and Zova `SITE_ID`; unique among enabled sites. |
51
+ | `<public-path>` | External mount path; aligned between Zova `APP_PUBLIC_PATH` and Vona `publicPath`; unique among enabled sites. |
52
+ | `<site-module>` | Vona module that owns the site bean and copied SSR assets. |
53
+ | `<bundle-path>` | Copied SSR release directory under `<site-module>/assets/site`; exactly matches Vona `bundlePath`. |
54
+ | `<zova-rest-package>` | Flavor-specific generated REST/type package imported by the Vona site bean. |
55
+ | `<root-build-wrapper>` | Root command that produces the selected SSR bundle **and** REST/type package. |
56
+
57
+ Only one enabled SSR site may own the empty/root public path. A non-root site’s route aliases are relative to the router mount base; do not repeat `<public-path>` inside those aliases.
58
+
59
+ ## Configure Zova
60
+
61
+ ### 1. Add a tracked flavor environment
62
+
63
+ Keep shared defaults in `zova/env/.env`; add the site’s tracked flavor override at:
64
+
65
+ ```text
66
+ zova/env/.env.<site-flavor>
67
+ ```
68
+
69
+ The override must select a non-empty `SITE_ID`, the matching `APP_PUBLIC_PATH`, an SSR rendering profile, and the build-copy targets. For a Vona-integrated SSR site, set `SSR_WITH_VONA=true`.
70
+
71
+ Choose `SSR_PROFILE` from the rendering contract:
72
+
73
+ - use a public profile for cookie-free, cache-safe first paint;
74
+ - choose a session/private profile deliberately for cookie-backed admission, personalized first paint, or private SSR data.
75
+
76
+ Point the build destinations to the selected Vona site module and generated package workspace:
77
+
78
+ ```text
79
+ BUILD_COPY_RELEASE = ../vona/src/.../modules/<site-module>/assets/site
80
+ BUILD_REST_COPY_DIST = ../vona/.zova-rest
81
+ ```
82
+
83
+ Do not change ports or worktree-local environment overrides to make a new site work. Follow the normal worktree-environment workflow only when that separate setup is explicitly required.
84
+
85
+ ### 2. Add flavor-specific frontend configuration only where necessary
86
+
87
+ Use `zova/src/front/config/config/config.<site-flavor>.ts` for site-specific route aliases, route exclusions, layouts, or app configuration. Keep shared API, locale, theme, SSR, and app assembly in base `config.ts`.
88
+
89
+ For mounted routing:
90
+
91
+ - configure an unnamed canonical route through `config.routes.path`;
92
+ - configure a named dynamic route through `config.routes.name`;
93
+ - generate a named alias with `$router.getAliasPath(...)` rather than assuming `$router.getPagePath(...)` selects an alias;
94
+ - do not duplicate the external mount prefix in an alias because `APP_PUBLIC_PATH` supplies that boundary.
95
+
96
+ Choose layouts, title, locales, assets, route exclusions, `requiresAuth`, and redirects from the new site’s contract. Existing Admin and Web sites are specimens, not defaults to copy blindly.
97
+
98
+ ### 3. Add durable scripts and root wrappers
99
+
100
+ Inspect the active root `package.json` and `zova/package.json` first. Add lower-level Zova scripts in the durable source manifest, normally including:
101
+
102
+ ```text
103
+ dev:ssr:<site-flavor>
104
+ build:ssr:<site-flavor>
105
+ build:rest:<site-flavor>
106
+ preview:ssr:<site-flavor>
107
+ ```
108
+
109
+ The SSR and REST scripts must pass the same flavor. Add a paired batch and an explicit root build wrapper that runs both outputs in order:
110
+
111
+ ```bash
112
+ npm run build:ssr:<site-flavor>
113
+ npm run build:rest:<site-flavor>
114
+ ```
115
+
116
+ Use the root wrapper for normal site-level builds and in Vona diagnostics. Add the flavor to an aggregate build only when it belongs to that edition’s default shipped artifact set.
117
+
118
+ Edit durable manifests such as root `package.json` and `zova/package.original.json`; do not hand-edit generated `zova/package.json`, generated REST output, copied bundles, or generated dependency state. Do not run `npm run init` merely to synchronize a manifest.
119
+
120
+ ## Register the Vona SSR site
121
+
122
+ Create or refine an independently packaged Vona site module. It must expose its assets, source entry point, locale metadata, and an SSR-site bean.
123
+
124
+ The site bean should:
125
+
126
+ 1. import `IPagePathRecord` and `IIconRecord` from `<zova-rest-package>`;
127
+ 2. augment the SSR site-ID and public-path type records;
128
+ 3. define typed page/page-data options around the generated route record;
129
+ 4. extend `BeanSsrSiteBase` and register `@SsrSite(...)`;
130
+ 5. provide a localized title, `<site-id>`, `<public-path>`, `<bundle-path>`, and `<root-build-wrapper>` diagnostics command.
131
+
132
+ Conceptually:
133
+
134
+ ```ts
135
+ @SsrSite({
136
+ siteId: '<site-id>',
137
+ publicPath: '<public-path>',
138
+ bundlePath: '<bundle-path>',
139
+ diagnostics: { buildCommand: 'npm run <root-build-wrapper>' },
140
+ })
141
+ export class SsrSiteExample extends BeanSsrSiteBase<...> {}
142
+ ```
143
+
144
+ The default release identity is normally `ssr-<site-flavor>-<app-version>`. Unless you intentionally override it, use that exact copied directory as `<bundle-path>`. Vona resolves the bundle from the owning site module’s `assets/site/<bundle-path>` directory.
145
+
146
+ Ensure the site module is discoverable through its durable suite/module manifest. Let normal tooling generate metadata and dependency closures; do not hand-edit generated metadata.
147
+
148
+ ## Build the artifact pair and sync Vona
149
+
150
+ For every Vona-consumed independent SSR site, this sequence is required:
151
+
152
+ ```bash
153
+ npm run <root-build-wrapper>
154
+ npm run deps:vona
155
+ ```
156
+
157
+ The wrapper must create both:
158
+
159
+ ```text
160
+ SSR bundle and client assets
161
+ + generated flavor REST/type package
162
+ ```
163
+
164
+ `build:rest:<site-flavor>` alone is not a valid Vona SSR handoff: Vona needs the bundle, assets, and typed REST package to move together. Likewise, a default Admin or Web wrapper does not prove an independently named site was rebuilt.
165
+
166
+ After generation, verify that:
167
+
168
+ - the copied release directory matches `<bundle-path>`;
169
+ - the generated package name and Vona import match `<zova-rest-package>`;
170
+ - the Vona local workspace can discover the package after `npm run deps:vona`;
171
+ - the site bean’s diagnostics command names the same root wrapper that produced the artifacts.
172
+
173
+ If the generated artifacts are correct and `npm run deps:vona` completed but Vona still sees stale local types, diagnose [local dependency drift](/fullstack/contract-loop-playbook#recovery-path-for-local-dependency-drift) only then.
174
+
175
+ ## Verify through the Vona boundary
176
+
177
+ A standalone Zova development server can help with page iteration, but it does not prove copied artifacts, Vona site matching, generated type handoff, or production-like hydration.
178
+
179
+ Run the narrowest meaningful checks first:
180
+
181
+ ```bash
182
+ npm run tsc:zova
183
+ npm run <root-build-wrapper>
184
+ npm run deps:vona
185
+ pnpm --dir vona run tsc
186
+ ```
187
+
188
+ Then prove the Vona-served site:
189
+
190
+ 1. request the exact public mount path and assert the intended Vona SSR site handled it;
191
+ 2. inspect raw HTML for server-rendered content and the correct cache/admission behavior;
192
+ 3. verify client assets load from the mounted site;
193
+ 4. open the same route in a browser and assert hydration completes without errors or SSR/client mismatch;
194
+ 5. verify canonical and alias route behavior, including no duplicated mount path;
195
+ 6. exercise the site’s public/private admission or redirect contract;
196
+ 7. retain focused browser/E2E evidence for the new site rather than relying only on compilation.
197
+
198
+ For more detail, read [SSR Architecture Overview](/frontend/ssr-architecture-overview), [SSR Build and Deploy Guide](/frontend/ssr-build-deploy-guide), [Vona + Zova Integration](/fullstack/vona-zova-integration), and [Contract Loop Playbook](/fullstack/contract-loop-playbook).
@@ -64,6 +64,8 @@ Because Start can differ in UI layer, module composition, SSR site baselines, an
64
64
  2. the Start repo’s `package.json`
65
65
  3. the exact Zova flavor names and generated output paths used there
66
66
 
67
+ For a new independently mounted SSR surface, use [Independent SSR Site and Flavor Setup](/fullstack/ssr-site-and-flavor-setup). It defines the linked Zova flavor, paired SSR/REST artifacts, Vona `@SsrSite` registration, and browser proof without assuming that either edition’s default Admin or Web wrapper applies.
68
+
67
69
  ## Recommended integration workflow
68
70
 
69
71
  ### 1. Detect the edition
@@ -0,0 +1,37 @@
1
+ import { defineConfig } from '@playwright/test';
2
+
3
+ const rootDir = new URL('../..', import.meta.url).pathname;
4
+ const baseURL = 'http://127.0.0.1:4173';
5
+
6
+ export default defineConfig({
7
+ testDir: `${rootDir}/repo-e2e/docs/specs`,
8
+ outputDir: `${rootDir}/repo-e2e/docs/test-results`,
9
+ fullyParallel: false,
10
+ workers: 1,
11
+ forbidOnly: !!process.env.CI,
12
+ retries: process.env.CI ? 2 : 0,
13
+ reporter: process.env.CI
14
+ ? [
15
+ ['html', { open: 'never', outputFolder: `${rootDir}/repo-e2e/docs/playwright-report` }],
16
+ ['list'],
17
+ ]
18
+ : 'list',
19
+ use: {
20
+ baseURL,
21
+ viewport: { width: 1440, height: 900 },
22
+ trace: 'on-first-retry',
23
+ },
24
+ webServer: {
25
+ command: 'pnpm --dir repo-docs docs:preview -- --host 127.0.0.1 --port 4173 --strictPort',
26
+ cwd: rootDir,
27
+ url: `${baseURL}/`,
28
+ timeout: 120_000,
29
+ reuseExistingServer: false,
30
+ stdout: 'pipe',
31
+ stderr: 'pipe',
32
+ gracefulShutdown: {
33
+ signal: 'SIGINT',
34
+ timeout: 10_000,
35
+ },
36
+ },
37
+ });
@@ -0,0 +1,180 @@
1
+ import type { Page } from '@playwright/test';
2
+
3
+ import { expect, test } from '@playwright/test';
4
+
5
+ interface IRectangle {
6
+ bottom: number;
7
+ height: number;
8
+ left: number;
9
+ right: number;
10
+ top: number;
11
+ width: number;
12
+ }
13
+
14
+ interface IBlogsGeometry {
15
+ cards: IRectangle[];
16
+ grid: IRectangle & { display: string };
17
+ }
18
+
19
+ interface IArticleGeometry {
20
+ aside: IRectangle;
21
+ container: IRectangle;
22
+ content: IRectangle;
23
+ contentContainer: IRectangle;
24
+ body: IRectangle;
25
+ }
26
+
27
+ const blogArticlePaths = [
28
+ '/blogs/ai-react-nextjs-enterprise-architecture-cabloy/',
29
+ '/blogs/cabloy-fullstack-resource-addressing/',
30
+ '/blogs/nextjs-integrated-fullstack-cabloy-contract-loop/',
31
+ '/blogs/vue-object-oriented-zova-beginner-mental-model/',
32
+ ];
33
+
34
+ function collectPageErrors(page: Page) {
35
+ const errors: Error[] = [];
36
+ page.on('pageerror', error => {
37
+ errors.push(error);
38
+ });
39
+ return errors;
40
+ }
41
+
42
+ function collectConsoleErrors(page: Page) {
43
+ const errors: string[] = [];
44
+ page.on('console', message => {
45
+ const text = message.text();
46
+ if (message.type() === 'error' || /hydration mismatch/i.test(text)) {
47
+ errors.push(text);
48
+ }
49
+ });
50
+ return errors;
51
+ }
52
+
53
+ function getDocumentHorizontalOverflow(page: Page) {
54
+ return page.evaluate(() => {
55
+ return (
56
+ Math.max(document.documentElement.scrollWidth, document.body.scrollWidth) - window.innerWidth
57
+ );
58
+ });
59
+ }
60
+
61
+ function getBlogsGeometry(page: Page) {
62
+ return page.locator('.cabloy-blog-grid').evaluate(grid => {
63
+ const toRectangle = (element: Element) => {
64
+ const { bottom, height, left, right, top, width } = element.getBoundingClientRect();
65
+ return { bottom, height, left, right, top, width };
66
+ };
67
+ const cards = Array.from(grid.querySelectorAll('.cabloy-blog-card'), toRectangle);
68
+ return {
69
+ grid: {
70
+ ...toRectangle(grid),
71
+ display: getComputedStyle(grid).display,
72
+ },
73
+ cards,
74
+ };
75
+ }) as Promise<IBlogsGeometry>;
76
+ }
77
+
78
+ function getArticleGeometry(page: Page) {
79
+ return page.locator('.VPDoc').evaluate(doc => {
80
+ const getRequiredChild = (parent: Element, selector: string) => {
81
+ const element = parent.querySelector(`:scope > ${selector}`);
82
+ if (!element) throw new Error(`Could not find ${selector}`);
83
+ return element;
84
+ };
85
+ const toRectangle = (element: Element) => {
86
+ const { bottom, height, left, right, top, width } = element.getBoundingClientRect();
87
+ return { bottom, height, left, right, top, width };
88
+ };
89
+ const container = getRequiredChild(doc, '.container');
90
+ const aside = getRequiredChild(container, '.aside');
91
+ const content = getRequiredChild(container, '.content');
92
+ const contentContainer = getRequiredChild(content, '.content-container');
93
+ const body = getRequiredChild(contentContainer, '.main').querySelector('.vp-doc');
94
+ if (!body) throw new Error('Could not find .vp-doc');
95
+ return {
96
+ aside: toRectangle(aside),
97
+ container: toRectangle(container),
98
+ content: toRectangle(content),
99
+ contentContainer: toRectangle(contentContainer),
100
+ body: toRectangle(body),
101
+ };
102
+ }) as Promise<IArticleGeometry>;
103
+ }
104
+
105
+ test(
106
+ 'DOCS-BLOGS-01: Blogs index uses a wide desktop card grid',
107
+ { tag: '@layout' },
108
+ async ({ page }) => {
109
+ const pageErrors = collectPageErrors(page);
110
+ const consoleErrors = collectConsoleErrors(page);
111
+
112
+ const documentResponse = await page.goto('/blogs/', { waitUntil: 'load' });
113
+ expect(documentResponse?.ok()).toBeTruthy();
114
+
115
+ await expect(page.locator('.Layout.cabloy-blogs-index')).toBeVisible();
116
+ await expect(page.getByRole('heading', { name: 'Blogs', level: 1 })).toBeVisible();
117
+ await expect(page.locator('.VPDoc.has-sidebar')).toHaveCount(0);
118
+ await expect(page.locator('.VPDoc.has-aside')).toHaveCount(0);
119
+
120
+ const cards = page.locator('.cabloy-blog-card');
121
+ await expect(cards).toHaveCount(4);
122
+ for (let index = 0; index < 4; index++) {
123
+ await expect(cards.nth(index)).toBeVisible();
124
+ await expect(cards.nth(index).locator('.cabloy-blog-card__cover img')).toBeVisible();
125
+ }
126
+
127
+ const geometry = await getBlogsGeometry(page);
128
+ expect(geometry.grid.display).toBe('grid');
129
+ expect(geometry.cards).toHaveLength(4);
130
+
131
+ const [firstCard, ...remainingCards] = geometry.cards;
132
+ expect(firstCard.width).toBeGreaterThanOrEqual(279);
133
+ for (const card of remainingCards) {
134
+ expect(Math.abs(card.top - firstCard.top)).toBeLessThanOrEqual(1);
135
+ expect(Math.abs(card.width - firstCard.width)).toBeLessThanOrEqual(1);
136
+ expect(card.left).toBeGreaterThan(firstCard.left);
137
+ expect(card.left).toBeGreaterThanOrEqual(geometry.grid.left - 1);
138
+ expect(card.right).toBeLessThanOrEqual(geometry.grid.right + 1);
139
+ expect(card.right).toBeLessThanOrEqual(1441);
140
+ }
141
+ expect(firstCard.left).toBeGreaterThanOrEqual(geometry.grid.left - 1);
142
+ expect(firstCard.right).toBeLessThanOrEqual(geometry.grid.right + 1);
143
+ expect(firstCard.right).toBeLessThanOrEqual(1441);
144
+
145
+ await expect.poll(() => getDocumentHorizontalOverflow(page)).toBeLessThanOrEqual(1);
146
+ expect(pageErrors).toEqual([]);
147
+ expect(consoleErrors).toEqual([]);
148
+ },
149
+ );
150
+
151
+ test(
152
+ 'DOCS-BLOGS-02: Blog articles use a wide desktop body with an outline aside',
153
+ { tag: '@layout' },
154
+ async ({ page }) => {
155
+ const pageErrors = collectPageErrors(page);
156
+ const consoleErrors = collectConsoleErrors(page);
157
+
158
+ for (const path of blogArticlePaths) {
159
+ const documentResponse = await page.goto(path, { waitUntil: 'load' });
160
+ expect(documentResponse?.ok()).toBeTruthy();
161
+
162
+ await expect(page.locator('.Layout.cabloy-blogs-article')).toBeVisible();
163
+ await expect(page.locator('.VPDoc.has-sidebar')).toHaveCount(0);
164
+ await expect(page.locator('.VPDoc.has-aside')).toHaveCount(1);
165
+ await expect(page.locator('.aside')).toBeVisible();
166
+
167
+ const geometry = await getArticleGeometry(page);
168
+ expect(geometry.container.width).toBeCloseTo(1216, 0);
169
+ expect(geometry.content.width).toBeCloseTo(960, 0);
170
+ expect(geometry.contentContainer.width).toBeCloseTo(896, 0);
171
+ expect(geometry.body.width).toBeCloseTo(896, 0);
172
+ expect(geometry.aside.left).toBeGreaterThanOrEqual(geometry.content.right - 1);
173
+ expect(geometry.aside.right).toBeLessThanOrEqual(geometry.container.right + 1);
174
+ await expect.poll(() => getDocumentHorizontalOverflow(page)).toBeLessThanOrEqual(1);
175
+ }
176
+
177
+ expect(pageErrors).toEqual([]);
178
+ expect(consoleErrors).toEqual([]);
179
+ },
180
+ );