cabloy 5.1.166 → 5.1.167

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 (48) 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 +1 -0
  4. package/CHANGELOG.md +13 -0
  5. package/package.json +1 -1
  6. package/repo-docs/frontend/page-params-guide.md +1 -1
  7. package/repo-docs/frontend/page-query-guide.md +6 -0
  8. package/repo-docs/frontend/zod-guide.md +9 -7
  9. package/repo-e2e/specs/routed-dialog.spec.ts +368 -0
  10. package/vona/pnpm-lock.yaml +6 -0
  11. package/zova/package.original.json +1 -1
  12. package/zova/packages-cli/cli/package.json +2 -2
  13. package/zova/packages-cli/cli-set-front/package.json +1 -1
  14. package/zova/packages-cli/cli-set-front/src/lib/bean/toolsMetadata/generateBeanGenerals.ts +1 -1
  15. package/zova/packages-cli/cli-set-front/src/lib/bean/toolsMetadata/generateScopeResources.ts +1 -1
  16. package/zova/packages-zova/zova/package.json +2 -2
  17. package/zova/pnpm-lock.yaml +2014 -2053
  18. package/zova/pnpm-workspace.yaml +1 -1
  19. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/index.ts +66 -18
  20. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialog.ts +9 -0
  21. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialogDetail.ts +23 -0
  22. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialogEntry.ts +23 -0
  23. package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialog/controller.tsx +301 -0
  24. package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialogDetail/controller.tsx +135 -0
  25. package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialogEntry/controller.tsx +108 -0
  26. package/zova/src/suite/a-demo/modules/demo-basic/src/routes.ts +19 -0
  27. package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/component/routedDialog.ts +63 -0
  28. package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/index.ts +44 -2
  29. package/zova/src/suite/cabloy-basic/modules/basic-app/src/bean/behavior.appModal.tsx +306 -18
  30. package/zova/src/suite/cabloy-basic/modules/basic-app/src/component/routedDialog/controller.tsx +51 -0
  31. package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/config.ts +19 -0
  32. package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/locale/en-us.ts +1 -0
  33. package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/locale/zh-cn.ts +1 -0
  34. package/zova/src/suite/cabloy-basic/modules/basic-app/src/lib/routedDialogHistory.ts +76 -0
  35. package/zova/src/suite/cabloy-basic/modules/basic-app/src/service/appModal.ts +156 -1
  36. package/zova/src/suite/cabloy-basic/modules/basic-app/src/types/appModal.ts +92 -13
  37. package/zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogHistory.test.ts +92 -0
  38. package/zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogRouter.test.ts +35 -0
  39. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  40. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +1 -1
  41. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  42. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  43. package/zova/src/suite-vendor/a-zova/modules/a-router/src/.metadata/index.ts +6 -6
  44. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +60 -3
  45. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +12 -0
  46. package/zova/src/suite-vendor/a-zova/modules/a-router/src/component/routerViewEmpty/controller.tsx +4 -1
  47. package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routerViewBase.tsx +5 -1
  48. package/zova/src/suite-vendor/a-zova/package.json +2 -2
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.166
1
+ 5.1.167
@@ -1 +1 @@
1
- {"sessionId":"ca46c3da-f068-490d-8585-52645b0c5e32","pid":48025,"procStart":"Wed Sep 9 23:18:11 2026","acquiredAt":1789174622732}
1
+ {"sessionId":"4dc366c1-f1a5-45c5-a2e1-5c89e5694587","pid":67516,"procStart":"Sat Sep 12 08:03:56 2026","acquiredAt":1789284193785}
@@ -165,6 +165,7 @@ Check whether the feature needs:
165
165
 
166
166
  - page route review
167
167
  - params/query schema alignment
168
+ - for numeric Zova page params and query fields, use `z.number()` and rely on the Cabloy/Zova route/query parse adapter; do not generalize this behavior to standalone Zod parsing or add manual coercion without a separate input-boundary requirement
168
169
  - effective layout, authentication, and SSR-profile default resolution
169
170
  - static-name and ordinary-alias exception review
170
171
  - alias or guard review
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.167
4
+
5
+ ### Features
6
+
7
+ - Add routed dialog support.
8
+ - Add update capabilities.
9
+ - Add bean general generation support.
10
+
11
+ ### Improvements
12
+
13
+ - Update resource handling.
14
+ - Update Zod documentation.
15
+
3
16
  ## 5.1.166
4
17
 
5
18
  ### Improvements
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.166",
3
+ "version": "5.1.167",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -26,7 +26,7 @@ export const ControllerPageCounterSchemaParams = z.object({
26
26
 
27
27
  This is more than a type annotation.
28
28
 
29
- Because route params arrive as strings at the URL level, the schema is also where Zova’s `z` wrapper can coerce the route value into the typed value that the page controller wants to consume.
29
+ Route params arrive as strings at the URL level. On Zova's route/query parsing path, the Cabloy Zod query adapter converts a numeric value before the `z.number()` validation runs, so a successful `$params.id` value is a number. Use `z.number()` for numeric page params; `z.coerce.number()` is normally redundant here. For the exact scope and nil/error semantics, see [Zova route/query number parsing](./zod-guide#zova-routequery-number-parsing).
30
30
 
31
31
  ## Route record requirements
32
32
 
@@ -31,6 +31,12 @@ export const ControllerPageCounterSchemaQuery = z.object({
31
31
 
32
32
  This matters because query parsing becomes typed and framework-aware instead of stringly-typed ad hoc access.
33
33
 
34
+ ## Numeric query values
35
+
36
+ Query values are commonly strings at the URL boundary. On Zova's route/query parsing path, the Cabloy Zod query adapter converts a non-nil numeric value with `Number(...)` before ordinary `z.number()` validation. Use `z.number()` for numeric query fields such as `age`; when parsing succeeds, `this.$query.age` is a number.
37
+
38
+ Invalid numeric text still fails number validation rather than silently producing a usable value. `z.coerce.number()` is normally redundant for Zova page query schemas, but may be appropriate at a different input boundary that does not use the Zova adapter. See [Zova route/query number parsing](/frontend/zod-guide#zova-routequery-number-parsing) for the adapter's nil semantics and scope.
39
+
34
40
  ## Use query values
35
41
 
36
42
  Representative pattern:
@@ -42,19 +42,21 @@ const user = z.object({
42
42
 
43
43
  This is the foundation used by typed page params and page query support.
44
44
 
45
- ## Type coercion
45
+ ## Zova route/query number parsing
46
46
 
47
- One of the most important Zova enhancements is automatic coercion.
47
+ Cabloy Basic pins `zod` in `zova/pnpm-workspace.yaml` to `npm:@cabloy/zod@4.3.8`. Zova's route/query parsing path uses the Cabloy Zod query adapter to prepare URL values before ordinary Zod validation.
48
48
 
49
- This matters because route params and query values often arrive as strings, while the page controller wants the final typed values.
50
-
51
- That is why schemas such as:
49
+ For a non-nil numeric route or query value, the adapter applies `Number(value)` before `z.number()` validates it. Therefore, use `z.number()` for numeric fields in Zova page params and query schemas:
52
50
 
53
51
  ```typescript
54
- z.number();
52
+ export const ControllerPageCounterSchemaQuery = z.object({
53
+ age: z.number().optional(),
54
+ });
55
55
  ```
56
56
 
57
- can still support route-driven values more naturally than raw parsing code.
57
+ When parsing succeeds, `this.$query.age` is a number. There is normally no need to add `z.coerce.number()` or a manual `Number(...)` conversion at this boundary.
58
+
59
+ The adapter preserves its nil semantics: an empty string, `'undefined'`, or `'null'` is handled as an absent or null value rather than as an ordinary number. Invalid numeric text becomes `NaN` and still fails normal number validation. This behavior belongs to Zova's route/query parsing path; do not infer it for an arbitrary standalone `Zod.parse` call. Use `z.coerce.number()` when handling a different input boundary that does not use the Zova adapter.
58
60
 
59
61
  ## Boolean coercion
60
62
 
@@ -0,0 +1,368 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+
3
+ import { expect, test } from '@playwright/test';
4
+
5
+ declare global {
6
+ interface Window {
7
+ __routedDialogHydrationWarnings?: unknown[];
8
+ }
9
+ }
10
+
11
+ function collectPageErrors(page: Page) {
12
+ const errors: Error[] = [];
13
+ page.on('pageerror', error => {
14
+ errors.push(error);
15
+ });
16
+ return errors;
17
+ }
18
+
19
+ function collectConsoleErrors(page: Page) {
20
+ const errors: string[] = [];
21
+ page.on('console', message => {
22
+ const text = message.text();
23
+ if (
24
+ message.type() === 'error' ||
25
+ (message.type() === 'warning' && /hydration (node |children )?mismatch/i.test(text))
26
+ ) {
27
+ errors.push(`${message.type()}: ${text}`);
28
+ }
29
+ });
30
+ return errors;
31
+ }
32
+
33
+ async function installHydrationWarningProbe(page: Page) {
34
+ await page.addInitScript(() => {
35
+ const originalWarn = console.warn;
36
+ const warnings: unknown[] = [];
37
+ Object.defineProperty(window, '__routedDialogHydrationWarnings', {
38
+ configurable: true,
39
+ value: warnings,
40
+ });
41
+ console.warn = (...args: unknown[]) => {
42
+ try {
43
+ if (
44
+ args.some(
45
+ arg => typeof arg === 'string' && /Hydration (node|children) mismatch/.test(arg),
46
+ )
47
+ ) {
48
+ const node = args.find(arg => arg instanceof Node) as Node | undefined;
49
+ warnings.push({
50
+ args: args.map(arg => (typeof arg === 'string' ? arg : String(arg))),
51
+ node: node
52
+ ? {
53
+ type: node.nodeType,
54
+ value: node.nodeValue,
55
+ parent: node.parentElement?.outerHTML.slice(0, 1000),
56
+ }
57
+ : undefined,
58
+ });
59
+ }
60
+ } finally {
61
+ originalWarn.apply(console, args as Parameters<typeof console.warn>);
62
+ }
63
+ };
64
+ });
65
+ }
66
+
67
+ async function getHydrationWarningProbe(page: Page) {
68
+ return await page.evaluate(() => window.__routedDialogHydrationWarnings ?? []);
69
+ }
70
+
71
+ function dialogByTitle(page: Page, dialog: 'A' | 'B') {
72
+ return page.getByRole('dialog').filter({
73
+ has: page.getByRole('heading', {
74
+ name: `Routed Dialog ${dialog}`,
75
+ exact: true,
76
+ }),
77
+ });
78
+ }
79
+
80
+ function routeRow(scope: Locator, name: string) {
81
+ return scope.locator('tbody tr').filter({ hasText: name }).last();
82
+ }
83
+
84
+ async function expectRouteValue(scope: Locator, name: string, value: string, type?: string) {
85
+ const row = routeRow(scope, name);
86
+ await expect(row.getByRole('cell').nth(1)).toHaveText(value);
87
+ if (type) {
88
+ await expect(row.getByRole('cell').nth(2)).toHaveText(type);
89
+ }
90
+ }
91
+
92
+ async function expectRoutePath(scope: Locator, pathname: string) {
93
+ const row = routeRow(scope, '$pageRoute.fullPath');
94
+ const fullPath = await row.getByRole('cell').nth(1).textContent();
95
+ expect(new URL(fullPath, 'http://e2e.invalid').pathname).toBe(pathname);
96
+ }
97
+
98
+ async function expectNoDialogLayout(dialog: Locator) {
99
+ await expect(dialog.locator('.drawer')).toHaveCount(0);
100
+ }
101
+
102
+ async function expectDialogMaxWidth(dialog: Locator, expected: string) {
103
+ await expect(dialog).toHaveCSS('max-width', expected);
104
+ }
105
+
106
+ async function getDialogTop(dialog: Locator) {
107
+ return Math.round((await dialog.boundingBox())?.y ?? -1);
108
+ }
109
+
110
+ async function expectDialogTop(dialog: Locator, expected: number) {
111
+ await expect.poll(() => getDialogTop(dialog)).toBe(expected);
112
+ }
113
+
114
+ async function openDemo(page: Page) {
115
+ const pageErrors = collectPageErrors(page);
116
+ const consoleErrors = collectConsoleErrors(page);
117
+ await installHydrationWarningProbe(page);
118
+ const response = await page.goto('/demo/basic/routedDialog', { waitUntil: 'load' });
119
+ expect(response?.ok()).toBeTruthy();
120
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
121
+ await expect(
122
+ page.getByRole('heading', { name: 'Routed Dialog Manual Validation', exact: true }),
123
+ ).toBeVisible();
124
+ const browserUrl = page.url();
125
+ await expect(
126
+ page
127
+ .getByRole('heading', { name: 'Main route sentinel', exact: true })
128
+ .locator('..')
129
+ .getByText(browserUrl, { exact: true }),
130
+ ).toBeVisible();
131
+ return { browserUrl, consoleErrors, pageErrors };
132
+ }
133
+
134
+ test(
135
+ 'ATP-BASIC-ROUTED-DIALOG-01: a local memory router navigates without changing the browser route',
136
+ { tag: ['@web', '@flow'] },
137
+ async ({ page }) => {
138
+ const { browserUrl, consoleErrors, pageErrors } = await openDemo(page);
139
+
140
+ await page.getByRole('button', { name: 'Open A', exact: true }).click();
141
+ const dialogA = dialogByTitle(page, 'A');
142
+ await expect(dialogA).toHaveCount(1);
143
+ await expect(
144
+ dialogA.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
145
+ ).toBeVisible();
146
+ await expectRoutePath(dialogA, '/demo/basic/routedDialogEntry');
147
+ await expectRouteValue(dialogA, '$query.dialog', 'A', 'string');
148
+ await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry', 'string');
149
+ await expectRouteValue(dialogA, '$query.via', 'open', 'string');
150
+ await expectRouteValue(dialogA, '$query.step', '0', 'number');
151
+ await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
152
+ await expectNoDialogLayout(dialogA);
153
+ expect(page.url()).toBe(browserUrl);
154
+
155
+ const entryTop = await getDialogTop(dialogA);
156
+ await dialogA.getByRole('button', { name: 'Local push to detail', exact: true }).click();
157
+ await expect(
158
+ dialogA.getByRole('heading', { name: 'Routed Dialog Detail', exact: true }),
159
+ ).toBeVisible();
160
+ await expectDialogTop(dialogA, entryTop);
161
+ await expectRoutePath(dialogA, '/demo/basic/routedDialogDetail/1');
162
+ await expectRouteValue(dialogA, '$params.id', '1', 'number');
163
+ await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry-push', 'string');
164
+ await expectRouteValue(dialogA, '$query.via', 'page-push', 'string');
165
+ await expectRouteValue(dialogA, '$query.step', '1', 'number');
166
+ await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
167
+ expect(page.url()).toBe(browserUrl);
168
+
169
+ await dialogA.getByRole('button', { name: 'Local replace next detail', exact: true }).click();
170
+ await expectRoutePath(dialogA, '/demo/basic/routedDialogDetail/11');
171
+ await expectRouteValue(dialogA, '$params.id', '11', 'number');
172
+ await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry-push-replace', 'string');
173
+ await expectRouteValue(dialogA, '$query.via', 'detail-replace', 'string');
174
+ await expectRouteValue(dialogA, '$query.step', '11', 'number');
175
+ await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
176
+ expect(page.url()).toBe(browserUrl);
177
+
178
+ await dialogA.getByRole('button', { name: 'Back', exact: true }).click();
179
+ await expect(
180
+ dialogA.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
181
+ ).toBeVisible();
182
+ await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
183
+ await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry', 'string');
184
+ expect(page.url()).toBe(browserUrl);
185
+
186
+ await dialogA.getByRole('button', { name: 'Local push to detail', exact: true }).click();
187
+ await expect(
188
+ dialogA.getByRole('heading', { name: 'Routed Dialog Detail', exact: true }),
189
+ ).toBeVisible();
190
+ await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
191
+ await dialogA.getByRole('button', { name: 'Local router.back', exact: true }).click();
192
+ await expect(
193
+ dialogA.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
194
+ ).toBeVisible();
195
+ await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
196
+ expect(page.url()).toBe(browserUrl);
197
+
198
+ await page.keyboard.press('Escape');
199
+ await expect(dialogA).toHaveCount(0);
200
+ expect(page.url()).toBe(browserUrl);
201
+ expect(pageErrors).toEqual([]);
202
+ expect(consoleErrors).toEqual([]);
203
+ expect(await getHydrationWarningProbe(page)).toEqual([]);
204
+ },
205
+ );
206
+
207
+ test(
208
+ 'ATP-BASIC-ROUTED-DIALOG-02: routed-dialog handles push and replace locally',
209
+ { tag: ['@web', '@flow'] },
210
+ async ({ page }) => {
211
+ const { browserUrl, consoleErrors, pageErrors } = await openDemo(page);
212
+ const dialogA = dialogByTitle(page, 'A');
213
+
214
+ await page.getByRole('button', { name: 'Open A with handle.push', exact: true }).click();
215
+ await expect(
216
+ dialogA.getByRole('heading', { name: 'Routed Dialog Detail', exact: true }),
217
+ ).toBeVisible();
218
+ await expectRoutePath(dialogA, '/demo/basic/routedDialogDetail/101');
219
+ await expectRouteValue(dialogA, '$params.id', '101', 'number');
220
+ await expectRouteValue(dialogA, '$query.token', 'dialog-a-handle-push', 'string');
221
+ await expectRouteValue(dialogA, '$query.via', 'handle-push', 'string');
222
+ await expectRouteValue(dialogA, '$query.step', '1', 'number');
223
+ await expect(page.getByText('A: handle.push', { exact: true })).toBeVisible();
224
+ await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
225
+ const handlePushTop = await getDialogTop(dialogA);
226
+ expect(page.url()).toBe(browserUrl);
227
+
228
+ await page.keyboard.press('Escape');
229
+ await expect(dialogA).toHaveCount(0);
230
+ expect(page.url()).toBe(browserUrl);
231
+
232
+ await page.getByRole('button', { name: 'Open A with handle.replace', exact: true }).click();
233
+ await expect(
234
+ dialogA.getByRole('heading', { name: 'Routed Dialog Detail', exact: true }),
235
+ ).toBeVisible();
236
+ await expectRoutePath(dialogA, '/demo/basic/routedDialogDetail/202');
237
+ await expectRouteValue(dialogA, '$params.id', '202', 'number');
238
+ await expectRouteValue(dialogA, '$query.token', 'dialog-a-handle-replace', 'string');
239
+ await expectRouteValue(dialogA, '$query.via', 'handle-replace', 'string');
240
+ await expectRouteValue(dialogA, '$query.step', '2', 'number');
241
+ await expect(page.getByText('A: handle.replace', { exact: true })).toBeVisible();
242
+ await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
243
+ await expectDialogTop(dialogA, handlePushTop);
244
+ expect(page.url()).toBe(browserUrl);
245
+
246
+ await page.keyboard.press('Escape');
247
+ await expect(dialogA).toHaveCount(0);
248
+ expect(page.url()).toBe(browserUrl);
249
+ expect(pageErrors).toEqual([]);
250
+ expect(consoleErrors).toEqual([]);
251
+ expect(await getHydrationWarningProbe(page)).toEqual([]);
252
+ },
253
+ );
254
+
255
+ test(
256
+ 'ATP-BASIC-ROUTED-DIALOG-04: routed dialogs apply responsive and scalar maximum widths',
257
+ { tag: ['@web', '@flow'] },
258
+ async ({ page }) => {
259
+ await page.setViewportSize({ width: 700, height: 900 });
260
+ const { browserUrl, consoleErrors, pageErrors } = await openDemo(page);
261
+ const dialogA = dialogByTitle(page, 'A');
262
+
263
+ await page.getByRole('button', { name: 'Open A', exact: true }).click();
264
+ await expect(dialogA).toHaveCount(1);
265
+ await expectDialogMaxWidth(dialogA, '640px');
266
+ await expectDialogTop(dialogA, 16);
267
+
268
+ await page.setViewportSize({ width: 900, height: 900 });
269
+ await expectDialogMaxWidth(dialogA, '768px');
270
+ await expectDialogTop(dialogA, 32);
271
+
272
+ await page.setViewportSize({ width: 1100, height: 900 });
273
+ await expectDialogMaxWidth(dialogA, '1024px');
274
+ await expectDialogTop(dialogA, 48);
275
+ await expectNoDialogLayout(dialogA);
276
+ expect(page.url()).toBe(browserUrl);
277
+
278
+ await page.keyboard.press('Escape');
279
+ await expect(dialogA).toHaveCount(0);
280
+
281
+ await page.getByRole('button', { name: 'Open A (720px width)', exact: true }).click();
282
+ await expect(dialogA).toHaveCount(1);
283
+ await expectDialogMaxWidth(dialogA, '720px');
284
+ await page.keyboard.press('Escape');
285
+ await expect(dialogA).toHaveCount(0);
286
+
287
+ await page.setViewportSize({ width: 900, height: 900 });
288
+ await page.getByRole('button', { name: 'Open A (md 800px width)', exact: true }).click();
289
+ await expect(dialogA).toHaveCount(1);
290
+ await expectDialogMaxWidth(dialogA, '800px');
291
+ await expectDialogTop(dialogA, 32);
292
+ await page.setViewportSize({ width: 700, height: 900 });
293
+ await expectDialogMaxWidth(dialogA, '640px');
294
+ await expectDialogTop(dialogA, 16);
295
+ await page.keyboard.press('Escape');
296
+ await expect(dialogA).toHaveCount(0);
297
+
298
+ await page.setViewportSize({ width: 900, height: 900 });
299
+ await page.getByRole('button', { name: 'Open A (80px top gutter)', exact: true }).click();
300
+ await expect(dialogA).toHaveCount(1);
301
+ await expectDialogTop(dialogA, 80);
302
+ await page.keyboard.press('Escape');
303
+ await expect(dialogA).toHaveCount(0);
304
+
305
+ await page.getByRole('button', { name: 'Open A (md 64px top gutter)', exact: true }).click();
306
+ await expect(dialogA).toHaveCount(1);
307
+ await expectDialogTop(dialogA, 64);
308
+ await page.setViewportSize({ width: 700, height: 900 });
309
+ await expectDialogTop(dialogA, 16);
310
+ expect(page.url()).toBe(browserUrl);
311
+
312
+ await page.keyboard.press('Escape');
313
+ await expect(dialogA).toHaveCount(0);
314
+ expect(pageErrors).toEqual([]);
315
+ expect(consoleErrors).toEqual([]);
316
+ expect(await getHydrationWarningProbe(page)).toEqual([]);
317
+ },
318
+ );
319
+
320
+ test(
321
+ 'ATP-BASIC-ROUTED-DIALOG-03: concurrent dialogs keep isolated routes and close last-opened first',
322
+ { tag: ['@web', '@flow'] },
323
+ async ({ page }) => {
324
+ const { browserUrl, consoleErrors, pageErrors } = await openDemo(page);
325
+
326
+ await page.getByRole('button', { name: 'Open A and B', exact: true }).click();
327
+ const dialogA = dialogByTitle(page, 'A');
328
+ const dialogB = dialogByTitle(page, 'B');
329
+ await expect(dialogA).toHaveCount(1);
330
+ await expect(dialogB).toHaveCount(1);
331
+ await expect(
332
+ dialogA.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
333
+ ).toBeVisible();
334
+ await expect(
335
+ dialogB.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
336
+ ).toBeVisible();
337
+ await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry', 'string');
338
+ await expectRouteValue(dialogB, '$query.token', 'dialog-b-entry', 'string');
339
+ await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
340
+ await expect(dialogB.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
341
+
342
+ await dialogB.getByRole('button', { name: 'Local push to detail', exact: true }).click();
343
+ await expect(
344
+ dialogB.getByRole('heading', { name: 'Routed Dialog Detail', exact: true }),
345
+ ).toBeVisible();
346
+ await expectRoutePath(dialogB, '/demo/basic/routedDialogDetail/1');
347
+ await expectRouteValue(dialogB, '$query.token', 'dialog-b-entry-push', 'string');
348
+ await expect(dialogB.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
349
+ await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
350
+ await expect(
351
+ dialogA.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
352
+ ).toBeVisible();
353
+ await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry', 'string');
354
+ expect(page.url()).toBe(browserUrl);
355
+
356
+ await page.keyboard.press('Escape');
357
+ await expect(dialogB).toHaveCount(0);
358
+ await expect(dialogA).toHaveCount(1);
359
+ expect(page.url()).toBe(browserUrl);
360
+
361
+ await page.keyboard.press('Escape');
362
+ await expect(dialogA).toHaveCount(0);
363
+ expect(page.url()).toBe(browserUrl);
364
+ expect(pageErrors).toEqual([]);
365
+ expect(consoleErrors).toEqual([]);
366
+ expect(await getHydrationWarningProbe(page)).toEqual([]);
367
+ },
368
+ );
@@ -545,6 +545,9 @@ importers:
545
545
 
546
546
  .zova-rest/cabloy-basic-admin:
547
547
  dependencies:
548
+ '@cabloy/vue-router':
549
+ specifier: ^4.4.16
550
+ version: 4.4.16(vue@3.5.39(typescript@5.9.3))
548
551
  '@tanstack/vue-query':
549
552
  specifier: ^5.101.1
550
553
  version: 5.101.2(vue@3.5.39(typescript@5.9.3))
@@ -623,6 +626,9 @@ importers:
623
626
 
624
627
  .zova-rest/cabloy-basic-web:
625
628
  dependencies:
629
+ '@cabloy/vue-router':
630
+ specifier: ^4.4.16
631
+ version: 4.4.16(vue@3.5.39(typescript@5.9.3))
626
632
  '@tanstack/vue-query':
627
633
  specifier: ^5.101.1
628
634
  version: 5.101.2(vue@3.5.39(typescript@5.9.3))
@@ -70,7 +70,7 @@
70
70
  "quasar": "^2.19.3",
71
71
  "tailwindcss": "^4.2.2",
72
72
  "vue": "^3.5.38",
73
- "vue-router": "npm:@cabloy/vue-router@^4.4.16",
73
+ "vue-router": "npm:@cabloy/vue-router@^4.4.18",
74
74
  "zova": "workspace:^"
75
75
  },
76
76
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-cli",
3
- "version": "1.2.107",
3
+ "version": "1.2.108",
4
4
  "gitHead": "6f675a8cc46d596142c591c28a40cc4d82fcc6cc",
5
5
  "description": "zova cli",
6
6
  "keywords": [
@@ -44,7 +44,7 @@
44
44
  "@cabloy/process-helper": "^3.1.8",
45
45
  "fs-extra": "^11.3.5",
46
46
  "semver": "^7.6.2",
47
- "zova-cli-set-front": "^1.2.105"
47
+ "zova-cli-set-front": "^1.2.106"
48
48
  },
49
49
  "devDependencies": {
50
50
  "clean-package": "^2.2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-cli-set-front",
3
- "version": "1.2.105",
3
+ "version": "1.2.106",
4
4
  "gitHead": "6f675a8cc46d596142c591c28a40cc4d82fcc6cc",
5
5
  "description": "zova cli-set-front",
6
6
  "keywords": [
@@ -16,7 +16,7 @@ export async function generateBeanGenerals(
16
16
  const beanFullName = `${moduleName}.${sceneName}.${beanName}`;
17
17
  if (isIgnore) continue;
18
18
  if (!sceneMeta.scopeResource) {
19
- contentImports.push(`import { ${className} } from '${fileNameJSRelative}';`);
19
+ contentImports.push(`import type { ${className} } from '${fileNameJSRelative}';`);
20
20
  }
21
21
  contentRecords.push(`'${beanFullName}': ${className};`);
22
22
  }
@@ -17,7 +17,7 @@ export async function generateScopeResources(
17
17
  for (const globFile of globFiles) {
18
18
  const { fileNameJSRelative, className, beanName, isIgnore } = globFile;
19
19
  if (isIgnore) continue;
20
- contentImports.push(`import { ${className} } from '${fileNameJSRelative}';`);
20
+ contentImports.push(`import type { ${className} } from '${fileNameJSRelative}';`);
21
21
  contentRecords.push(`'${beanName}': ${className};`);
22
22
  }
23
23
  if (contentImports.length === 0) return '';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova",
3
- "version": "5.1.158",
3
+ "version": "5.1.159",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A vue3 framework with ioc",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "zova-core": "^5.1.89",
49
- "zova-suite-a-zova": "^5.1.157"
49
+ "zova-suite-a-zova": "^5.1.158"
50
50
  },
51
51
  "devDependencies": {
52
52
  "clean-package": "^2.2.0",