cabloy 5.1.174 → 5.1.176

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 (86) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/scheduled_tasks.lock +1 -1
  3. package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +4 -0
  4. package/CHANGELOG.md +25 -0
  5. package/package.json +2 -2
  6. package/repo-docs/.vitepress/config.mjs +7 -0
  7. package/repo-docs/frontend/a-model-under-the-hood.md +11 -0
  8. package/repo-docs/frontend/behavior-guide.md +73 -0
  9. package/repo-docs/frontend/column-configuration-guide.md +254 -0
  10. package/repo-docs/frontend/component-guide.md +2 -29
  11. package/repo-docs/frontend/controller-boundary-guide.md +229 -0
  12. package/repo-docs/frontend/introduction.md +4 -0
  13. package/repo-docs/frontend/page-route-guide.md +6 -0
  14. package/repo-docs/frontend/resource-picker-guide.md +223 -0
  15. package/repo-docs/frontend/rest-resource-source-reading-map.md +37 -21
  16. package/repo-docs/frontend/rest-resource-under-the-hood.md +50 -30
  17. package/repo-docs/frontend/routed-dialog-guide.md +471 -0
  18. package/repo-docs/frontend/router-stack-guide.md +4 -2
  19. package/repo-docs/frontend/router-view-hosts-guide.md +3 -2
  20. package/repo-docs/frontend/ssr-client-only.md +6 -0
  21. package/repo-docs/frontend/table-guide.md +2 -1
  22. package/repo-docs/frontend/table-resource-crud-cookbook.md +1 -0
  23. package/repo-docs/frontend/use-state-data-best-practices.md +26 -0
  24. package/repo-docs/fullstack/a-pay-payment-suite.md +31 -17
  25. package/repo-e2e/specs/cabloy-basic.spec.ts +11 -0
  26. package/repo-e2e/specs/controller-boundary.spec.ts +1 -2
  27. package/test-results/.last-run.json +1 -1
  28. package/vona/packages-cli/cli/package.json +1 -1
  29. package/vona/packages-cli/cli-set-api/package.json +1 -1
  30. package/vona/packages-vona/vona/package.json +1 -1
  31. package/vona/packages-vona/vona-core/package.json +1 -1
  32. package/vona/packages-vona/vona-core/src/lib/utils/util.ts +1 -0
  33. package/vona/packages-vona/vona-mock/package.json +1 -1
  34. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +1 -1
  35. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/package.json +1 -1
  36. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/test/layoutprofile.test.ts +2 -3
  37. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  38. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  39. package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
  40. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/service/executor.ts +6 -1
  41. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  42. package/zova/package.original.json +1 -1
  43. package/zova/packages-zova/zova/package.json +2 -2
  44. package/zova/pnpm-lock.yaml +45 -38
  45. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +4 -3
  46. package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +4 -3
  47. package/zova/src/suite/a-commerce/modules/commerce-trade/package.json +3 -1
  48. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionAdjustStock/controller.tsx +3 -2
  49. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +16 -20
  50. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionShip/controller.tsx +3 -2
  51. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +6 -5
  52. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +5 -4
  53. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +4 -4
  54. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +1 -1
  55. package/zova/src/suite/a-demo/modules/demo-basic/src/page/controllerBoundary/controller.tsx +2 -2
  56. package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionDeleteForce.tsx +4 -4
  57. package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +4 -4
  58. package/zova/src/suite/cabloy-basic/modules/basic-app/src/monkey.ts +11 -0
  59. package/zova/src/suite/cabloy-basic/modules/basic-app/src/types/appModal.ts +1 -0
  60. package/zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogContext.test.ts +60 -0
  61. package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionUpdate.tsx +4 -4
  62. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/actionCreate/controller.tsx +4 -4
  63. package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/page/dashboard/controller.tsx +8 -2
  64. package/zova/src/{suite-vendor/a-zova/modules/a-boundary → suite/cabloy-basic/modules/basic-pay}/package.json +13 -5
  65. package/zova/src/{suite-vendor/a-pay/modules/a-pay → suite/cabloy-basic/modules/basic-pay}/src/.metadata/component/paymentNextAction.ts +2 -2
  66. package/zova/src/suite/cabloy-basic/modules/basic-pay/src/.metadata/index.ts +68 -0
  67. package/zova/src/suite/cabloy-basic/modules/basic-pay/src/.metadata/this.ts +2 -0
  68. package/zova/src/{suite-vendor/a-pay/modules/a-pay → suite/cabloy-basic/modules/basic-pay}/src/component/paymentNextAction/controller.tsx +13 -24
  69. package/zova/src/suite/cabloy-basic/modules/basic-table/src/bean/tableCell.actionUpdate.tsx +4 -4
  70. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionCreate/controller.tsx +4 -4
  71. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionDeleteBulk/controller.tsx +4 -4
  72. package/zova/src/suite/cabloy-basic/package.json +1 -0
  73. package/zova/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  74. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +4 -46
  75. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +13 -0
  76. package/zova/src/suite-vendor/a-pay/package.json +2 -2
  77. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  78. package/zova/src/suite-vendor/a-zova/modules/a-router/src/monkey.ts +17 -3
  79. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +1 -2
  80. package/zova/src/suite-vendor/a-zova/package.json +3 -4
  81. package/zova/src/suite-vendor/a-zova/modules/a-boundary/LICENSE +0 -21
  82. package/zova/src/suite-vendor/a-zova/modules/a-boundary/src/.metadata/index.ts +0 -26
  83. package/zova/src/suite-vendor/a-zova/modules/a-boundary/src/.metadata/this.ts +0 -2
  84. /package/zova/src/{suite-vendor/a-zova/modules/a-boundary → suite/cabloy-basic/modules/basic-pay}/src/index.ts +0 -0
  85. /package/zova/src/{suite-vendor/a-zova/modules/a-boundary → suite/cabloy-basic/modules/basic-pay}/tsconfig.build.json +0 -0
  86. /package/zova/src/{suite-vendor/a-zova/modules/a-boundary → suite/cabloy-basic/modules/basic-pay}/tsconfig.json +0 -0
@@ -0,0 +1,471 @@
1
+ # Routed Dialog Guide
2
+
3
+ A routed dialog hosts ordinary Cabloy pages in an isolated dialog-local router without changing the main browser route or URL.
4
+
5
+ Use `this.$appModal.routedDialog(...)` when one interaction needs page routing inside a modal: for example, a multi-step picker, a detail-to-detail flow, or a focused page-level form. It is supported by both **Cabloy Basic** and **Cabloy Start**, through their edition-local app modules.
6
+
7
+ ## Choose the right primitive
8
+
9
+ Use a normal `this.$appModal.dialog(...)` when the caller already owns the content and only needs to render supplied dialog VNodes.
10
+
11
+ Use `routedDialog(...)` when the content should be an ordinary page route with its existing controller, typed params and query, guards, and page-level navigation.
12
+
13
+ Use a normal application page route instead when the flow must own a shareable, reloadable, or directly accessible browser URL. A routed dialog deliberately has no browser-address-bar ownership.
14
+
15
+ A routed dialog is also not a Router Tabs or application-shell replacement. It is an embedded, per-dialog page host that uses a local route stack.
16
+
17
+ ## The mental model
18
+
19
+ Opening a routed dialog does the following:
20
+
21
+ ```text
22
+ caller page
23
+ -> $appModal.routedDialog(...)
24
+ -> one embedded BeanRouter + one memory history per dialog
25
+ -> an ordinary target page hosted by ZRouterViewStack
26
+ -> dialog-local navigation, result, and cleanup
27
+ ```
28
+
29
+ Each instance has its own router, memory history, Router Stack scene, and dialog context. It shares the application's registered route records and normal route guards, but it does not share route state with the main router or another routed-dialog instance.
30
+
31
+ Consequently:
32
+
33
+ - `push`, `replace`, Back, and `RouterLink` inside the dialog do not change the browser URL;
34
+ - opening a second routed dialog does not change the first dialog's route or Back state;
35
+ - the target page runs through the normal Zova page route pipeline, including generated `$params` and `$query` schemas;
36
+ - the dialog host supplies the modal chrome, so the target page is rendered without the normal generated application layout wrapper.
37
+
38
+ ## Define ordinary target routes
39
+
40
+ A routed-dialog target is an ordinary module page route. Do not invent a separate dialog-only route format.
41
+
42
+ For a static target, keep the usual static-route convention: omit `name` unless the route has a documented named-route requirement.
43
+
44
+ ```typescript
45
+ import { ZPageCustomerPicker } from './.metadata/page/customerPicker.js';
46
+
47
+ export const routes: IModuleRoute[] = [
48
+ {
49
+ path: 'customerPicker',
50
+ component: ZPageCustomerPicker,
51
+ },
52
+ ];
53
+ ```
54
+
55
+ For a route with dynamic params, continue to declare `name`. This is required for the generated typed `$params` contract.
56
+
57
+ ```typescript
58
+ import { ZPageCustomerDetail } from './.metadata/page/customerDetail.js';
59
+
60
+ export const routes: IModuleRoute[] = [
61
+ {
62
+ name: 'sales:customerDetail',
63
+ path: 'customer/:id',
64
+ component: ZPageCustomerDetail,
65
+ },
66
+ ];
67
+ ```
68
+
69
+ See [Page Route Guide](/frontend/page-route-guide), [Page Params Guide](/frontend/page-params-guide), and [Page Query Guide](/frontend/page-query-guide) for the general route rules. Hosting a route in a dialog does not relax those rules.
70
+
71
+ ## Open a routed dialog
72
+
73
+ For a static route, use `$router.getPagePath(...)` to create the initial route. The caller still uses its normal application router to construct the target location; the returned route is then installed in the dialog-local router.
74
+
75
+ ```typescript
76
+ public openCustomerPicker() {
77
+ const route = this.$router.getPagePath('/sales/customerPicker', {
78
+ query: {
79
+ source: 'invoice',
80
+ },
81
+ });
82
+
83
+ this.$appModal.routedDialog({
84
+ route,
85
+ });
86
+ }
87
+ ```
88
+
89
+ For a dynamic target, use the named route and params.
90
+
91
+ ```typescript
92
+ const handle = this.$appModal.routedDialog({
93
+ route: {
94
+ name: 'sales:customerDetail',
95
+ params: { id: customerId },
96
+ query: { source: 'invoice' },
97
+ },
98
+ });
99
+ ```
100
+
101
+ The first argument describes the routed workflow:
102
+
103
+ - `route` is the required initial `RouteLocationRaw`;
104
+ - `title` and `icon` control dialog header content;
105
+ - `props` supplies workflow configuration;
106
+ - `session` supplies dialog-private mutable session state;
107
+ - `createPageHostProviders` adapts the generic dialog context to a feature-specific page host;
108
+ - `onClose` receives a close notification.
109
+
110
+ Use locale-generated text for user-visible titles and labels in production code. Do not add a hard-coded display string merely to name a dialog.
111
+
112
+ ## Use the handle
113
+
114
+ `routedDialog(...)` returns immediately, while the embedded router initializes asynchronously.
115
+
116
+ ```typescript
117
+ const handle = this.$appModal.routedDialog<TResult, TProps, TSession>(options, dialogOptions);
118
+
119
+ await handle.ready;
120
+ const result = await handle.result;
121
+ ```
122
+
123
+ The handle has these public members:
124
+
125
+ | Member | Purpose |
126
+ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127
+ | `ready` | Resolves after the local router has initialized and reached its initial route. Rejects if initialization fails or the dialog closes before it becomes ready. |
128
+ | `result` | Resolves to `TResult` after the hosted workflow calls `resolve(value)`. Resolves to `undefined` after an ordinary close or cancel. Rejects when initialization fails. |
129
+ | `push(to)` | Pushes a route onto this dialog's local history. It waits for `ready` internally. |
130
+ | `replace(to)` | Replaces the current local route. It waits for `ready` internally. |
131
+ | `close()` | Closes the dialog without a business result. |
132
+
133
+ Use `await handle.ready` when the caller must observe successful initialization before doing other work. Calling and awaiting `handle.push(...)` or `handle.replace(...)` is sufficient when navigation is the only next step, because both methods wait for readiness internally.
134
+
135
+ Handle navigation remains local:
136
+
137
+ ```typescript
138
+ await handle.push({
139
+ name: 'sales:customerDetail',
140
+ params: { id: customerId },
141
+ });
142
+
143
+ await handle.replace({
144
+ name: 'sales:customerDetail',
145
+ params: { id: replacementCustomerId },
146
+ });
147
+ ```
148
+
149
+ After the dialog closes, `push` and `replace` reject instead of navigating a disposed router.
150
+
151
+ ## Navigate inside hosted pages
152
+
153
+ A page hosted by a routed dialog continues to use ordinary Zova routing APIs. There is no second dialog-specific navigation API.
154
+
155
+ ```typescript
156
+ public openDetail() {
157
+ const route = this.$router.getPagePath('/sales/customer/:id', {
158
+ params: { id: this.customerId },
159
+ query: {
160
+ source: this.$query.source,
161
+ },
162
+ });
163
+ return this.$router.push(route);
164
+ }
165
+
166
+ public replaceDetail(id: number) {
167
+ return this.$router.replace({
168
+ name: 'sales:customerDetail',
169
+ params: { id },
170
+ });
171
+ }
172
+
173
+ public goBack() {
174
+ this.$router.back();
175
+ }
176
+ ```
177
+
178
+ `RouterLink` follows the same dialog-local router automatically.
179
+
180
+ ```tsx
181
+ <RouterLink to={this.entryRoute}>Return to the picker</RouterLink>
182
+ ```
183
+
184
+ The initial destination is installed with local `replace(...)`, making it the root of the local history:
185
+
186
+ - initial open has no Back entry;
187
+ - local `push(...)` creates a Back entry;
188
+ - local `replace(...)` does not create a Back entry;
189
+ - the dialog header shows Back only when `showBackButton` is enabled and the local history can go back;
190
+ - header Back and `this.$router.back()` navigate local history only; neither turns into a close action at the local root.
191
+
192
+ The demo route at `/demo/basic/routedDialog` demonstrates page-local `push`, `replace`, `back`, and `RouterLink`, including typed params and query values.
193
+
194
+ ## Return a typed result
195
+
196
+ The three type parameters describe a complete dialog workflow:
197
+
198
+ - `TResult`: the value returned to the caller;
199
+ - `TProps`: read-mostly configuration supplied when opening the dialog;
200
+ - `TSession`: mutable state shared by pages in this one dialog instance.
201
+
202
+ A caller can await a typed result and preserve its existing state on any normal close:
203
+
204
+ ```typescript
205
+ interface ICustomerSelection {
206
+ id: string;
207
+ title: string;
208
+ }
209
+
210
+ interface ICustomerPickerProps {
211
+ initialCustomerId?: string;
212
+ }
213
+
214
+ interface ICustomerPickerSession {
215
+ selectedCustomerId?: string;
216
+ }
217
+
218
+ private async _consumeCustomerPicker(
219
+ handle: IRoutedDialogHandle<ICustomerSelection>,
220
+ ) {
221
+ const result = await handle.result;
222
+ if (!result) return;
223
+
224
+ this.customerId = result.id;
225
+ }
226
+
227
+ public openCustomerPicker() {
228
+ const handle = this.$appModal.routedDialog<
229
+ ICustomerSelection,
230
+ ICustomerPickerProps,
231
+ ICustomerPickerSession
232
+ >({
233
+ route: this.$router.getPagePath('/sales/customerPicker'),
234
+ props: { initialCustomerId: this.customerId },
235
+ session: { selectedCustomerId: this.customerId },
236
+ });
237
+
238
+ void this._consumeCustomerPicker(handle);
239
+ }
240
+ ```
241
+
242
+ The routed-dialog host injects an `IRoutedDialogContext` under `routedDialogContextKey`. Every Zova Bean exposes the current host context through the optional `$routedDialog` shortcut:
243
+
244
+ ```typescript
245
+ public confirm(customer: ICustomerSelection) {
246
+ this.$routedDialog?.resolve(customer);
247
+ }
248
+
249
+ public cancel() {
250
+ this.$routedDialog?.cancel();
251
+ }
252
+ ```
253
+
254
+ `$routedDialog` performs a live host lookup on every access. It is `undefined` outside a routed-dialog host, and it follows the current host when a Bean is reused across host lifecycles. Do not retain a previously read context as a cross-host or cross-lifecycle snapshot; read `this.$routedDialog` at the point where the operation is needed.
255
+
256
+ `resolve(value)` settles `handle.result` with `value` and closes the dialog. `cancel()` closes it and leaves `handle.result` as `undefined`.
257
+
258
+ > [!IMPORTANT]
259
+ > `undefined` does not mean only an explicit `cancel()`. Closing with the header button, Escape, an enabled backdrop, or `handle.close()` also resolves an unfinished `result` as `undefined`. If business logic must distinguish outcomes, define that distinction in the workflow result or feature session; `onClose` does not expose a close reason.
260
+
261
+ `onClose` is useful for cleanup or notification, but it is not a substitute for the typed `result` channel. For a reusable business page, prefer the feature-specific host adapter described in the next section rather than coupling the page directly to `IRoutedDialogContext`.
262
+
263
+ ## Pass props, session, and a page host
264
+
265
+ `props` and `session` are not URL state:
266
+
267
+ - use route params and query for route-addressable state;
268
+ - use `props` for input configuration of one dialog workflow;
269
+ - use `session` for mutable state shared across pages and rerenders in that dialog;
270
+ - use `result` for the final value returned to the caller.
271
+
272
+ When a routed page needs feature-specific dialog behavior, create a feature-specific page-host adapter. The current resource picker uses this pattern: `basic-resource` opens the dialog and creates the host, while the `rest-resource` picker page consumes the host contract.
273
+
274
+ ```typescript
275
+ const handle = this.$appModal.routedDialog<
276
+ IResourceTableSelectionPayload,
277
+ IResourcePickerPageOptions,
278
+ IResourcePickerPageSession
279
+ >({
280
+ route: {
281
+ name: 'rest-resource:resourcePicker',
282
+ params: { resource: this.resource },
283
+ },
284
+ props: options,
285
+ session,
286
+ createPageHostProviders: dialog => ({
287
+ [resourcePickerPageHostKey]: createResourcePickerPageHost(dialog),
288
+ }),
289
+ });
290
+
291
+ const result = await handle.result;
292
+ ```
293
+
294
+ The adapter converts the generic dialog contract into the feature contract:
295
+
296
+ ```typescript
297
+ export function createResourcePickerPageHost(
298
+ dialog: IRoutedDialogContext<
299
+ IResourceTableSelectionPayload,
300
+ IResourcePickerPageOptions,
301
+ IResourcePickerPageSession
302
+ >,
303
+ ): IResourcePickerPageHost {
304
+ if (!dialog.props || !dialog.session) {
305
+ throw new Error('resource picker requires dialog options and session');
306
+ }
307
+
308
+ return {
309
+ options: dialog.props,
310
+ session: dialog.session,
311
+ resolve: selection => dialog.resolve(selection),
312
+ cancel: () => dialog.cancel(),
313
+ };
314
+ }
315
+ ```
316
+
317
+ The hosted page consumes the explicit host-scoped contract:
318
+
319
+ ```typescript
320
+ @Use({ name: resourcePickerPageHostKey, injectionScope: 'host' })
321
+ $$pickerHost: IResourcePickerPageHost | undefined;
322
+ ```
323
+
324
+ This keeps a reusable target page coupled to its own feature contract instead of to a generic modal implementation. A page that requires such a host is an internal workflow page: a route record alone does not make it a suitable direct browser entry.
325
+
326
+ > [!TIP]
327
+ > If a workflow declares `TSession`, always provide an initial `session` object. The current input type makes `session` optional, while page-host code that needs it must validate it at runtime.
328
+
329
+ ## Configure presentation
330
+
331
+ Pass all presentation and close settings in the **second** argument.
332
+
333
+ ```typescript
334
+ const handle = this.$appModal.routedDialog(
335
+ {
336
+ route,
337
+ // Use the generated locale key for the actual workflow title.
338
+ title: this.scope.locale.SelectCustomer(),
339
+ },
340
+ {
341
+ maxWidth: { default: 640, md: 768, lg: 960 },
342
+ maxHeight: 'calc(100vh - 3rem)',
343
+ topGutter: { default: 16, md: 32 },
344
+ closeOnBackdrop: false,
345
+ closeOnEscape: true,
346
+ showCloseButton: true,
347
+ showBackButton: true,
348
+ },
349
+ );
350
+ ```
351
+
352
+ The presentation options are:
353
+
354
+ | Option | Meaning |
355
+ | ----------------- | ----------------------------------------------------------------------------- |
356
+ | `maxWidth` | A number, a CSS size string, or responsive `{ default, md, lg }` widths. |
357
+ | `maxHeight` | A number or CSS size string. |
358
+ | `topGutter` | A number, a CSS size string, or responsive `{ default, md, lg }` top gutters. |
359
+ | `closeOnBackdrop` | Whether a backdrop click closes the dialog. |
360
+ | `closeOnEscape` | Whether Escape closes the topmost dialog. |
361
+ | `showCloseButton` | Whether to display the header close button. |
362
+ | `showBackButton` | Whether to allow the header Back button when local history can go back. |
363
+
364
+ Numbers become pixel values. In the current Cabloy Basic implementation, the responsive keys use Tailwind breakpoints: `md` is `48rem` and `lg` is `64rem`.
365
+
366
+ The current Cabloy Basic defaults are:
367
+
368
+ | Option | Default |
369
+ | ----------------- | ------------------------------------- |
370
+ | `maxWidth` | `{ default: 640, md: 768, lg: 1024 }` |
371
+ | `topGutter` | `{ default: 16, md: 32, lg: 48 }` |
372
+ | `maxHeight` | `'calc(100vh - 2rem)'` |
373
+ | `closeOnBackdrop` | `false` |
374
+ | `closeOnEscape` | `true` |
375
+ | `showCloseButton` | `true` |
376
+ | `showBackButton` | `true` |
377
+
378
+ > [!WARNING]
379
+ > `IModalRoutedDialogOptions` currently structurally includes presentation fields in its first argument, but current option resolution is not uniform there. Use the first argument only for route/workflow data and put **all** presentation settings in the second argument. This is the safe public convention for the current implementation.
380
+
381
+ These defaults and breakpoint details are Cabloy Basic presentation behavior. Do not treat them as edition-neutral Zova guarantees.
382
+
383
+ ## Client-only and SSR boundary
384
+
385
+ `routedDialog` is explicitly client-only. Its embedded router initialization rejects outside `process.env.CLIENT`.
386
+
387
+ Therefore:
388
+
389
+ - open it from a user interaction in the browser in the normal case;
390
+ - do not open it in a server-rendered page's server execution path or SSR initialization branch;
391
+ - when lifecycle-driven opening is genuinely needed, defer it to an explicit post-hydration client boundary such as `this.$ssr.handleDirectOrOnHydrated(...)`;
392
+ - do not infer that an already-open routed dialog can be server-rendered merely because its host page supports SSR.
393
+
394
+ A Vona integrated SSR page or a Zova standalone SSR page can hydrate successfully and subsequently open a routed dialog on the client. That verifies the host-page hydration boundary; it does not make the dialog router server-renderable.
395
+
396
+ `ClientOnly` is a render boundary for browser-only UI. It does not make a server-side call to `this.$appModal.routedDialog(...)` valid. See [SSR ClientOnly](/frontend/ssr-client-only) and [SSR Architecture Overview](/frontend/ssr-architecture-overview).
397
+
398
+ ## Lifecycle and cleanup
399
+
400
+ The observable lifecycle is:
401
+
402
+ ```text
403
+ loading -> ready -> closed
404
+ loading -> error -> closed
405
+ ```
406
+
407
+ While loading, the modal renders a loading state. On initialization failure, `ready` and `result` reject and the dialog renders an error state until it is closed.
408
+
409
+ A normal close is idempotent and disposes the embedded router and memory history. It also removes the dialog from the modal stack, so Escape always applies only to the most recently opened eligible modal.
410
+
411
+ When several dialogs are open, each has independent route visits, Back state, page instances, and cleanup. The only shared surface is the application route table and application-level services.
412
+
413
+ ## Common mistakes
414
+
415
+ ### Treating it as a URL navigation feature
416
+
417
+ A routed dialog uses memory history. Its local route is not the browser route, so do not use it for a flow that must survive reloads, be bookmarked, or be sent to someone as a URL.
418
+
419
+ ### Adding a special page-navigation API
420
+
421
+ Do not create one. Hosted pages use the normal `$router`, `RouterLink`, `$params`, and `$query` APIs; the host scope supplies the local router automatically.
422
+
423
+ ### Expecting Back to close the root page
424
+
425
+ Back is navigation, not close. At the local root it is hidden. Offer an explicit workflow cancellation or close action when the user needs to leave the dialog.
426
+
427
+ ### Storing non-URL workflow state in query
428
+
429
+ Use `props` and `session` for private workflow configuration/state, and use `result` for completion. Keep params and query for route-addressable state.
430
+
431
+ ### Calling it during SSR
432
+
433
+ Do not call it from server execution. Defer opening to a client interaction or an explicit post-hydration boundary.
434
+
435
+ ## Verification
436
+
437
+ After changing a routed-dialog workflow, verify the smallest relevant surface first:
438
+
439
+ 1. Open the existing `/demo/basic/routedDialog` demo after hydration.
440
+ 2. Confirm that opening, local `push`, local `replace`, local Back, and `RouterLink` do not change the main browser URL.
441
+ 3. Confirm that `push` enables Back but `replace` from the local root does not.
442
+ 4. Open two dialogs and confirm that navigation in one does not alter the other.
443
+ 5. Confirm that successful completion returns the expected result and ordinary close/cancel returns `undefined`.
444
+ 6. Verify any host-dependent target page only receives the required page-host contract through its intended routed-dialog entry path.
445
+
446
+ The source-level history and router coverage lives in:
447
+
448
+ - `zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogHistory.test.ts`
449
+ - `zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogRouter.test.ts`
450
+ - `repo-e2e/specs/routed-dialog.spec.ts`
451
+
452
+ For the source-confirmed production pattern, read:
453
+
454
+ - `zova/src/suite/cabloy-basic/modules/basic-resource/src/component/formFieldResourcePicker/controller.tsx`
455
+ - `zova/src/suite/cabloy-basic/modules/basic-resource/src/lib/resourcePickerPageHost.ts`
456
+ - `zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/resourcePicker/controller.tsx`
457
+
458
+ ## Read together with
459
+
460
+ - [Page Route Guide](/frontend/page-route-guide)
461
+ - [Page Params Guide](/frontend/page-params-guide)
462
+ - [Page Query Guide](/frontend/page-query-guide)
463
+ - [A-Router Guide](/frontend/a-router-guide)
464
+ - [Router View Hosts Guide](/frontend/router-view-hosts-guide)
465
+ - [Router Stack Guide](/frontend/router-stack-guide)
466
+ - [Module Scope](/frontend/module-scope)
467
+ - [SSR ClientOnly](/frontend/ssr-client-only)
468
+
469
+ ## Final takeaway
470
+
471
+ A routed dialog is a client-only, isolated page-routing workflow inside modal chrome. Reuse ordinary page routes and ordinary Zova navigation; use typed props, session, page hosts, and results to express the workflow contract; and use the main router only for the browser route outside the dialog.
@@ -184,9 +184,10 @@ In the current public Basic source:
184
184
 
185
185
  - Router Stack exists as a real framework primitive
186
186
  - the currently visible public Basic shell is centered on Router Tabs
187
- - Router Stack should therefore be read as an available routed-host strategy, not as the main visible Basic shell pattern
187
+ - `basic-app:routedDialog` is an embedded, per-dialog `ZRouterViewStack` consumer rather than an application layout consumer
188
+ - Router Stack should therefore be read as an available routed-host strategy, not as the main visible Basic shell pattern or a Router Tabs replacement
188
189
 
189
- This keeps the docs source-confirmed without overstating current public usage.
190
+ For the routed-dialog workflow, local memory history, and client-only boundary, see [Routed Dialog Guide](/frontend/routed-dialog-guide). This keeps the docs source-confirmed without overstating current public usage.
190
191
 
191
192
  ## Where Router Stack stops and other docs begin
192
193
 
@@ -212,6 +213,7 @@ Use this page together with:
212
213
  - [Router Tabs Mechanism](/frontend/router-tabs-mechanism)
213
214
  - [Router Tabs vs Stack](/frontend/router-tabs-vs-stack)
214
215
  - [A-Router Guide](/frontend/a-router-guide)
216
+ - [Routed Dialog Guide](/frontend/routed-dialog-guide)
215
217
  - [Zova Source Reading Map](/frontend/zova-source-reading-map)
216
218
 
217
219
  ## Final takeaway
@@ -11,6 +11,7 @@ Read this page together with:
11
11
  - [Router Tabs Introduction](/frontend/router-tabs-introduction)
12
12
  - [Router Tabs vs Stack](/frontend/router-tabs-vs-stack)
13
13
  - [Router Stack Guide](/frontend/router-stack-guide)
14
+ - [Routed Dialog Guide](/frontend/routed-dialog-guide)
14
15
  - [Router Tabs Mechanism](/frontend/router-tabs-mechanism)
15
16
  - [Page Meta Guide](/frontend/page-meta-guide)
16
17
  - [Router Tabs Layout Integration](/frontend/router-tabs-layout-integration)
@@ -353,9 +354,9 @@ If your next question is specifically about how a page author should update task
353
354
 
354
355
  In the current public Cabloy Basic source, there is no app-level layout consumer of `ZRouterViewStack` outside the vendor module itself.
355
356
 
356
- That means the stack host is present as a reusable framework primitive, but the current public Basic layouts visibly consume `routerViewTabs` rather than `routerViewStack`.
357
+ That does not mean the Stack host has no application use. `basic-app:routedDialog` is an embedded, per-dialog consumer: each routed dialog creates its own local router and stack scene, then hosts ordinary pages in `ZRouterViewStack`. It is not an application layout and does not add Router Tabs/workbench semantics. See [Routed Dialog Guide](/frontend/routed-dialog-guide) for the user-facing workflow.
357
358
 
358
- This is a source-confirmed statement based on the current repo search surface, not a guarantee about all future editions or downstream apps.
359
+ The current public Basic layouts visibly consume `routerViewTabs`, while routed dialogs use Stack as an isolated page host. This is a source-confirmed statement based on the current repo search surface, not a guarantee about all future editions or downstream apps.
359
360
 
360
361
  ## Empty vs tabs vs stack
361
362
 
@@ -33,6 +33,12 @@ This is one of the simplest but most important SSR boundary tools.
33
33
 
34
34
  It makes the server/client split explicit and keeps browser-only behavior from leaking into the server render path.
35
35
 
36
+ ## Routed dialogs are client-only
37
+
38
+ `this.$appModal.routedDialog(...)` is a runtime-enforced client-only API in the current Cabloy Basic implementation. Open it from a browser interaction in the normal case, and do not call it from a server-rendered execution path merely because the target page or its host supports SSR.
39
+
40
+ If lifecycle-driven opening is genuinely required, defer it to an explicit post-hydration client boundary. `ClientOnly` protects browser-only rendering; it does not make a server-side routed-dialog call valid. See [Routed Dialog Guide](/frontend/routed-dialog-guide) for the local-router, result, and hydration boundary.
41
+
36
42
  ## Implementation checks for client-only SSR boundaries
37
43
 
38
44
  When adding or editing SSR-sensitive UI, ask whether the component depends on client-only behavior such as browser APIs, client-only rendering expectations, or interactions that should not appear in the server render.
@@ -16,6 +16,7 @@ Use this page together with:
16
16
  - [API Schema Guide](/frontend/api-schema-guide)
17
17
  - [Bean Scene Authoring](/frontend/bean-scene-authoring)
18
18
  - [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern)
19
+ - [Column Configuration Guide](/frontend/column-configuration-guide)
19
20
  - [TableCell Authoring Cookbook](/frontend/table-cell-cookbook)
20
21
  - [Table + Resource CRUD Cookbook](/frontend/table-resource-crud-cookbook)
21
22
  - [Zova Table Under the Hood](/frontend/zova-table-under-the-hood)
@@ -247,7 +248,7 @@ For the schema side of that contract, also see [API Schema Guide](/frontend/api-
247
248
 
248
249
  ## Persisted per-user column layouts
249
250
 
250
- `ZovaRender.column(...)` defines the schema defaults for column visibility, order, width, and fixed regions. On a standard resource list page, an authenticated user can personalize those defaults through the **Column Configuration** toolbar action. The saved layout is scoped to that user and the current page route path, then restored on later visits.
251
+ `ZovaRender.visible(...)` and schema/table-scene metadata decide which columns are eligible and establish their schema defaults; `ZovaRender.column(...)` supplies physical column defaults such as width and fixed regions. On a standard resource list page, an authenticated user can personalize eligible columns through the **Column Configuration** toolbar action. The saved layout is scoped to that user and the current page route path, then restored on later visits. For the complete setup, persistence, reconciliation, and Basic/Start guidance, see [Column Configuration Guide](/frontend/column-configuration-guide).
251
252
 
252
253
  | Edition | Toolbar block | Column-configuration action |
253
254
  | ------------ | ----------------------------- | -------------------------------- |
@@ -12,6 +12,7 @@ Use this page when your next question is practical rather than framework-neutral
12
12
  Use this page together with:
13
13
 
14
14
  - [Table Guide](/frontend/table-guide)
15
+ - [Column Configuration Guide](/frontend/column-configuration-guide)
15
16
  - [TableCell Authoring Cookbook](/frontend/table-cell-cookbook)
16
17
  - [Zova Table Under the Hood](/frontend/zova-table-under-the-hood)
17
18
  - [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern)
@@ -288,6 +288,32 @@ When a query-backed dialog needs readiness before it opens, await the existing q
288
288
 
289
289
  Do not copy an awaited `refetch()` result into a second long-lived controller/render state that drives an open dialog or persistent component. If the UI remains mounted and displays query-backed data, bind it to `query.data` or a model-derived reactive surface so later refetches and model updates remain visible.
290
290
 
291
+ ### Choose the refetch error boundary
292
+
293
+ A failed `await query.refetch()` normally resolves with a query result. When this one interaction must make a local decision, inspect `result.error`; mounted UI continues to render the query-owned `query.error` and `query.data` surfaces:
294
+
295
+ ```ts
296
+ const result = await query.refetch();
297
+ if (result.error) {
298
+ this.recoveryError = result.error;
299
+ return;
300
+ }
301
+ ```
302
+
303
+ Use `throwOnError: true` only when the interaction intentionally delegates an uncaught failure to an outer `try`/`catch` or action boundary. In Cabloy Basic, return or await that promise from `ZButton onPerform` so `BehaviorPerform` can invoke `onError` or show its default error alert:
304
+
305
+ ```tsx
306
+ <ZButton
307
+ onPerform={async () => {
308
+ await query.refetch({ throwOnError: true });
309
+ }}
310
+ >
311
+ {this.scope.locale.Refresh()}
312
+ </ZButton>
313
+ ```
314
+
315
+ Choose exactly one presentation owner for a failed refetch: the generic `ZButton` fallback, a local `onError`/`catch`, or query/local error UI. Do not add `throwOnError: true` merely because a refetch appears in `onPerform` when the current layer already classifies `result.error` or renders a domain-specific `query.error` state; otherwise one failure can produce duplicate generic and local feedback. `throwOnError` changes promise propagation, not query ownership. For the button lifecycle, see the [Behavior Guide](/frontend/behavior-guide#perform-a-standalone-asynchronous-button-action).
316
+
291
317
  ### Per-fetch persistence bypass
292
318
 
293
319
  When one interaction needs an API-fresh result but should not restore persisted data or schedule a persistence save for that fetch, use the model query's per-fetch option: