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
@@ -0,0 +1,108 @@
1
+ import { z } from 'zod';
2
+ import { BeanControllerPageBase } from 'zova';
3
+ import { Controller } from 'zova-module-a-bean';
4
+ import { ZPage } from 'zova-module-home-base';
5
+
6
+ export const ControllerPageRoutedDialogEntrySchemaParams = z.object({});
7
+
8
+ export const ControllerPageRoutedDialogEntrySchemaQuery = z.object({
9
+ dialog: z.enum(['A', 'B']).optional().default('A'),
10
+ token: z.string().optional().default('entry'),
11
+ via: z.string().optional().default('open'),
12
+ step: z.number().optional().default(0),
13
+ });
14
+
15
+ @Controller()
16
+ export class ControllerPageRoutedDialogEntry extends BeanControllerPageBase {
17
+ pushDetail() {
18
+ const route = this.$router.getPagePath('/demo/basic/routedDialogDetail/:id', {
19
+ params: { id: this.$query.step + 1 },
20
+ query: {
21
+ ...this.$query,
22
+ token: `${this.$query.token}-push`,
23
+ via: 'page-push',
24
+ step: this.$query.step + 1,
25
+ },
26
+ });
27
+ this.$router.push(route);
28
+ }
29
+
30
+ replaceDetail() {
31
+ const route = this.$router.getPagePath('/demo/basic/routedDialogDetail/:id', {
32
+ params: { id: this.$query.step + 10 },
33
+ query: {
34
+ ...this.$query,
35
+ token: `${this.$query.token}-replace`,
36
+ via: 'page-replace',
37
+ step: this.$query.step + 10,
38
+ },
39
+ });
40
+ this.$router.replace(route);
41
+ }
42
+
43
+ protected render() {
44
+ return (
45
+ <ZPage>
46
+ <div class="flex flex-col gap-4 p-1">
47
+ <div>
48
+ <h1 class="text-xl font-bold">Routed Dialog Entry</h1>
49
+ <p class="text-base-content/70">This page is rendered by the dialog-local router.</p>
50
+ </div>
51
+
52
+ <div class="overflow-x-auto">
53
+ <table class="table">
54
+ <thead>
55
+ <tr>
56
+ <th>Name</th>
57
+ <th>Value</th>
58
+ <th>Type</th>
59
+ </tr>
60
+ </thead>
61
+ <tbody>
62
+ <tr>
63
+ <td>$pageRoute.fullPath</td>
64
+ <td class="break-all">{this.$pageRoute?.fullPath}</td>
65
+ <td>string</td>
66
+ </tr>
67
+ <tr>
68
+ <td>$route.fullPath</td>
69
+ <td class="break-all">{this.$route.fullPath}</td>
70
+ <td>string</td>
71
+ </tr>
72
+ <tr>
73
+ <td>$query.dialog</td>
74
+ <td>{this.$query.dialog}</td>
75
+ <td>{typeof this.$query.dialog}</td>
76
+ </tr>
77
+ <tr>
78
+ <td>$query.token</td>
79
+ <td>{this.$query.token}</td>
80
+ <td>{typeof this.$query.token}</td>
81
+ </tr>
82
+ <tr>
83
+ <td>$query.via</td>
84
+ <td>{this.$query.via}</td>
85
+ <td>{typeof this.$query.via}</td>
86
+ </tr>
87
+ <tr>
88
+ <td>$query.step</td>
89
+ <td>{this.$query.step}</td>
90
+ <td>{typeof this.$query.step}</td>
91
+ </tr>
92
+ </tbody>
93
+ </table>
94
+ </div>
95
+
96
+ <div class="flex flex-wrap gap-2">
97
+ <button class="btn btn-primary" onClick={() => this.pushDetail()}>
98
+ Local push to detail
99
+ </button>
100
+ <button class="btn btn-secondary" onClick={() => this.replaceDetail()}>
101
+ Local replace to detail
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </ZPage>
106
+ );
107
+ }
108
+ }
@@ -2,6 +2,9 @@ import type { IModuleRoute } from 'zova-module-a-router';
2
2
 
3
3
  import { ZPageComponent } from './.metadata/page/component.js';
4
4
  import { ZPageLocale } from './.metadata/page/locale.js';
5
+ import { ZPageRoutedDialog } from './.metadata/page/routedDialog.js';
6
+ import { ZPageRoutedDialogDetail } from './.metadata/page/routedDialogDetail.js';
7
+ import { ZPageRoutedDialogEntry } from './.metadata/page/routedDialogEntry.js';
5
8
  import { ZPageRouteParams } from './.metadata/page/routeParams.js';
6
9
  import { ZPageRouteQuery } from './.metadata/page/routeQuery.js';
7
10
  import { ZPageRouteQueryB } from './.metadata/page/routeQueryB.js';
@@ -70,4 +73,20 @@ export const routes: IModuleRoute[] = [
70
73
  ssrProfile: 'public',
71
74
  },
72
75
  },
76
+ {
77
+ path: 'routedDialog',
78
+ component: ZPageRoutedDialog,
79
+ meta: { requiresAuth: false },
80
+ },
81
+ {
82
+ path: 'routedDialogEntry',
83
+ component: ZPageRoutedDialogEntry,
84
+ meta: { requiresAuth: false },
85
+ },
86
+ {
87
+ name: 'routedDialogDetail',
88
+ path: 'routedDialogDetail/:id',
89
+ component: ZPageRoutedDialogDetail,
90
+ meta: { requiresAuth: false },
91
+ },
73
92
  ];
@@ -0,0 +1,63 @@
1
+ import type { TypeControllerInnerProps } from 'zova';
2
+
3
+ import {
4
+ routeLocationKey,
5
+ routerKey,
6
+ routerViewLocationKey,
7
+ viewDepthKey,
8
+ } from '@cabloy/vue-router';
9
+ import { computed, defineComponent, provide, shallowReactive } from 'vue';
10
+ import { prepareComponentOptions, useController } from 'zova';
11
+
12
+ import type { ControllerRoutedDialogProps } from '../../component/routedDialog/controller.jsx';
13
+
14
+ import { ControllerRoutedDialog } from '../../component/routedDialog/controller.jsx';
15
+ export type ZRoutedDialogProps = {
16
+ controllerRef?: (ref: ControllerRoutedDialog) => void;
17
+ } & ControllerRoutedDialogProps;
18
+
19
+ type ControllerInnerProps = TypeControllerInnerProps<
20
+ ControllerRoutedDialogProps,
21
+ keyof typeof ControllerRoutedDialog.$propsDefault
22
+ >;
23
+ declare module 'zova-module-basic-app' {
24
+ export interface ControllerRoutedDialog {
25
+ $props: ControllerInnerProps;
26
+ }
27
+ }
28
+
29
+ export const ZRoutedDialog = defineComponent(
30
+ (props: ZRoutedDialogProps) => {
31
+ const router = props.item.state.router;
32
+ if (router) {
33
+ const currentRoute = router.currentRoute;
34
+ const reactiveRoute = {} as Record<string, unknown>;
35
+ for (const key in currentRoute.value) {
36
+ Object.defineProperty(reactiveRoute, key, {
37
+ get: () => currentRoute.value[key],
38
+ enumerable: true,
39
+ });
40
+ }
41
+ provide(routerKey, router.router);
42
+ provide(routeLocationKey, shallowReactive(reactiveRoute) as any);
43
+ provide(routerViewLocationKey, currentRoute);
44
+ provide(
45
+ viewDepthKey,
46
+ computed(() => router.getEmbeddedRouterViewDepth(currentRoute.value)),
47
+ );
48
+ }
49
+ useController(ControllerRoutedDialog, undefined, undefined);
50
+ return () => {};
51
+ },
52
+ {
53
+ ...prepareComponentOptions(ControllerRoutedDialog.$componentOptions),
54
+ props: {
55
+ item: { type: Object, required: true },
56
+ },
57
+ },
58
+ );
59
+ declare module 'zova-module-a-bean' {
60
+ export interface IVonaComponentRecord {
61
+ 'basic-app:routedDialog': ControllerRoutedDialogProps;
62
+ }
63
+ }
@@ -26,7 +26,7 @@ declare module 'zova-module-basic-app' {
26
26
  }
27
27
  /** service: end */
28
28
  /** service: begin */
29
- import { ServiceAppModal } from '../service/appModal.js';
29
+ import type { ServiceAppModal } from '../service/appModal.js';
30
30
  import 'zova';
31
31
  declare module 'zova' {
32
32
  export interface IBeanRecordGeneral {
@@ -34,6 +34,48 @@ declare module 'zova' {
34
34
  }
35
35
  }
36
36
  /** service: end */
37
+ /** controller: begin */
38
+ export * from '../component/routedDialog/controller.jsx';
39
+
40
+ import 'zova';
41
+ declare module 'zova' {
42
+
43
+
44
+ }
45
+ declare module 'zova-module-basic-app' {
46
+
47
+ export interface ControllerRoutedDialog {
48
+ /** @internal */
49
+ get scope(): ScopeModuleBasicApp;
50
+ }
51
+ }
52
+ /** controller: end */
53
+ /** controller: begin */
54
+ import type { ControllerRoutedDialog } from '../component/routedDialog/controller.jsx';
55
+ import 'zova';
56
+ declare module 'zova' {
57
+ export interface IBeanRecordLocal {
58
+ 'basic-app.controller.routedDialog': ControllerRoutedDialog;
59
+ }
60
+ }
61
+ /** controller: end */
62
+
63
+ /** components: begin */
64
+ export * from './component/routedDialog.js';
65
+ import { ZRoutedDialog } from './component/routedDialog.js';
66
+ export const components = {
67
+ 'routedDialog': ZRoutedDialog,
68
+ };
69
+ import 'zova';
70
+ declare module 'zova' {
71
+ export interface IComponentRecord {
72
+ 'basic-app:routedDialog': ControllerRoutedDialog;
73
+ }
74
+ export interface IZovaComponentRecord {
75
+ 'basic-app:routedDialog': typeof ZRoutedDialog;
76
+ }
77
+ }
78
+ /** components: end */
37
79
  /** behavior: begin */
38
80
  export * from '../bean/behavior.appModal.jsx';
39
81
  import { IBehaviorOptionsAppModal } from '../bean/behavior.appModal.jsx';
@@ -61,7 +103,7 @@ declare module 'zova-module-basic-app' {
61
103
  }
62
104
  /** behavior: end */
63
105
  /** behavior: begin */
64
- import { BehaviorAppModal } from '../bean/behavior.appModal.jsx';
106
+ import type { BehaviorAppModal } from '../bean/behavior.appModal.jsx';
65
107
  import 'zova';
66
108
  declare module 'zova' {
67
109
  export interface IBeanRecordLocal {
@@ -15,7 +15,14 @@ import {
15
15
  IModalItem,
16
16
  IModalMessageOptions,
17
17
  IModalPromptOptionsInner,
18
+ IModalResponsiveMaxWidth,
19
+ IModalResponsiveTopGutter,
20
+ IModalRoutedDialogItem,
21
+ IModalRoutedDialogPresentationOptions,
22
+ ModalRoutedDialogMaxWidth,
23
+ ModalRoutedDialogTopGutter,
18
24
  ModalType,
25
+ ModalWidth,
19
26
  } from '../types/appModal.js';
20
27
 
21
28
  export interface IBehaviorPropsInputAppModal {}
@@ -24,17 +31,92 @@ export interface IBehaviorPropsOutputAppModal extends IBehaviorPropsInputAppModa
24
31
 
25
32
  export interface IBehaviorOptionsAppModal extends IDecoratorBehaviorOptions {}
26
33
 
34
+ interface IPreparedDialogOptions extends Omit<IModalDialogOptions, 'maxWidth'> {
35
+ maxWidth?: ModalRoutedDialogMaxWidth;
36
+ topGutter?: ModalRoutedDialogTopGutter;
37
+ showBackButton?: boolean;
38
+ }
39
+
27
40
  interface IRenderDialogBaseOptions {
28
41
  modalItem: IModalItem;
29
- dialogOptions: IModalDialogOptions;
42
+ dialogOptions: IPreparedDialogOptions;
30
43
  iconName?: keyof IIconRecord;
31
44
  title: string;
32
45
  body?: VNode;
33
46
  actions?: VNode;
47
+ showBackButton?: boolean;
34
48
  showCloseButton?: boolean;
49
+ onBack?: () => void;
35
50
  onClose: () => void;
36
51
  }
37
52
 
53
+ interface IModalCardPresentation {
54
+ className?: string;
55
+ style?: Record<string, string>;
56
+ }
57
+
58
+ interface IModalOuterPresentation {
59
+ className?: string;
60
+ style?: Record<string, string>;
61
+ }
62
+
63
+ const responsiveMaxWidthClass = {
64
+ maxWidth: 'var(--zova-routed-dialog-max-width-default)',
65
+ $nest: {
66
+ '@media (min-width: 48rem)': {
67
+ maxWidth:
68
+ 'var(--zova-routed-dialog-max-width-md, var(--zova-routed-dialog-max-width-default))',
69
+ },
70
+ '@media (min-width: 64rem)': {
71
+ maxWidth:
72
+ 'var(--zova-routed-dialog-max-width-lg, var(--zova-routed-dialog-max-width-md, var(--zova-routed-dialog-max-width-default)))',
73
+ },
74
+ },
75
+ };
76
+
77
+ const responsiveTopGutterClass = {
78
+ paddingTop: 'var(--zova-routed-dialog-top-gutter-default)',
79
+ $nest: {
80
+ '@media (min-width: 48rem)': {
81
+ paddingTop:
82
+ 'var(--zova-routed-dialog-top-gutter-md, var(--zova-routed-dialog-top-gutter-default))',
83
+ },
84
+ '@media (min-width: 64rem)': {
85
+ paddingTop:
86
+ 'var(--zova-routed-dialog-top-gutter-lg, var(--zova-routed-dialog-top-gutter-md, var(--zova-routed-dialog-top-gutter-default)))',
87
+ },
88
+ },
89
+ };
90
+
91
+ const routedDialogMaxHeightClass = {
92
+ maxHeight:
93
+ 'min(var(--zova-routed-dialog-max-height, 100%), calc(100vh - var(--zova-routed-dialog-top-gutter-default) - 1rem))',
94
+ $nest: {
95
+ '@media (min-width: 48rem)': {
96
+ maxHeight:
97
+ 'min(var(--zova-routed-dialog-max-height, 100%), calc(100vh - var(--zova-routed-dialog-top-gutter-md, var(--zova-routed-dialog-top-gutter-default)) - 1rem))',
98
+ },
99
+ '@media (min-width: 64rem)': {
100
+ maxHeight:
101
+ 'min(var(--zova-routed-dialog-max-height, 100%), calc(100vh - var(--zova-routed-dialog-top-gutter-lg, var(--zova-routed-dialog-top-gutter-md, var(--zova-routed-dialog-top-gutter-default))) - 1rem))',
102
+ },
103
+ '@supports (height: 100dvh)': {
104
+ maxHeight:
105
+ 'min(var(--zova-routed-dialog-max-height, 100%), calc(100dvh - var(--zova-routed-dialog-top-gutter-default) - 1rem))',
106
+ $nest: {
107
+ '@media (min-width: 48rem)': {
108
+ maxHeight:
109
+ 'min(var(--zova-routed-dialog-max-height, 100%), calc(100dvh - var(--zova-routed-dialog-top-gutter-md, var(--zova-routed-dialog-top-gutter-default)) - 1rem))',
110
+ },
111
+ '@media (min-width: 64rem)': {
112
+ maxHeight:
113
+ 'min(var(--zova-routed-dialog-max-height, 100%), calc(100dvh - var(--zova-routed-dialog-top-gutter-lg, var(--zova-routed-dialog-top-gutter-md, var(--zova-routed-dialog-top-gutter-default))) - 1rem))',
114
+ },
115
+ },
116
+ },
117
+ },
118
+ };
119
+
38
120
  @Behavior<IBehaviorOptionsAppModal>()
39
121
  export class BehaviorAppModal extends BeanBehaviorBase<
40
122
  IBehaviorOptionsAppModal,
@@ -108,6 +190,7 @@ export class BehaviorAppModal extends BeanBehaviorBase<
108
190
  if (modalItem.type === 'alert') return this._renderAppModalAlert(modalItem);
109
191
  if (modalItem.type === 'confirm') return this._renderAppModalConfirm(modalItem);
110
192
  if (modalItem.type === 'prompt') return this._renderAppModalPrompt(modalItem);
193
+ if (modalItem.type === 'routedDialog') return this._renderAppModalRoutedDialog(modalItem);
111
194
  return this._renderAppModalDialog(modalItem);
112
195
  }
113
196
 
@@ -249,6 +332,41 @@ export class BehaviorAppModal extends BeanBehaviorBase<
249
332
  });
250
333
  }
251
334
 
335
+ private _renderAppModalRoutedDialog(modalItem: IModalRoutedDialogItem) {
336
+ const options = modalItem.options;
337
+ const dialogOptions = this._prepareDialogOptions(
338
+ modalItem.type,
339
+ modalItem.dialogOptions,
340
+ options,
341
+ );
342
+ const title = options.title ?? this.sys.env.APP_TITLE ?? '';
343
+ const state = modalItem.state;
344
+ let body: VNode;
345
+ if (state.status === 'error') {
346
+ body = <div class="text-error whitespace-pre-wrap">{String(state.error)}</div>;
347
+ } else if (state.status === 'ready' && state.router) {
348
+ const RoutedDialog = this.$zovaComponent('basic-app:routedDialog');
349
+ body = <RoutedDialog item={modalItem}></RoutedDialog>;
350
+ } else {
351
+ body = <div class="loading loading-spinner"></div>;
352
+ }
353
+ return this._renderDialogBase({
354
+ modalItem,
355
+ dialogOptions,
356
+ iconName: options.icon,
357
+ title,
358
+ body,
359
+ showBackButton: dialogOptions.showBackButton && state.status === 'ready' && state.canGoBack,
360
+ showCloseButton: dialogOptions.showCloseButton,
361
+ onBack: () => {
362
+ state.router?.back();
363
+ },
364
+ onClose: () => {
365
+ this.$appModal.close(modalItem.id, 'button');
366
+ },
367
+ });
368
+ }
369
+
252
370
  private _renderAppModalDialog(modalItem: IModalItem) {
253
371
  const options = modalItem.options as IModalDialogRenderOptions | undefined;
254
372
  const dialogOptions = this._prepareDialogOptions(modalItem.type, modalItem.dialogOptions);
@@ -275,12 +393,27 @@ export class BehaviorAppModal extends BeanBehaviorBase<
275
393
  title,
276
394
  body,
277
395
  actions,
396
+ showBackButton,
278
397
  showCloseButton,
398
+ onBack,
279
399
  onClose,
280
400
  }: IRenderDialogBaseOptions) {
281
- const style = this._dialogStyle(dialogOptions);
401
+ const routedDialog = modalItem.type === 'routedDialog';
402
+ const presentation = this._dialogPresentation(dialogOptions, routedDialog);
403
+ const outerPresentation = routedDialog
404
+ ? this._routedDialogOuterPresentation(dialogOptions)
405
+ : undefined;
282
406
  return (
283
- <div key={modalItem.id} class="fixed inset-0 z-50 flex items-center justify-center p-4">
407
+ <div
408
+ key={modalItem.id}
409
+ class={this.$cssMerge(
410
+ routedDialog
411
+ ? 'fixed inset-0 z-50 flex items-start justify-center overflow-y-auto px-4 pb-4'
412
+ : 'fixed inset-0 z-50 flex items-center justify-center p-4',
413
+ outerPresentation?.className,
414
+ )}
415
+ style={outerPresentation?.style}
416
+ >
284
417
  <div
285
418
  class="absolute inset-0 bg-base-content/30"
286
419
  onClick={() => {
@@ -292,11 +425,29 @@ export class BehaviorAppModal extends BeanBehaviorBase<
292
425
  <div
293
426
  role="dialog"
294
427
  aria-modal="true"
295
- class="card bg-base-100 shadow-2xl relative w-full max-h-[calc(100vh-2rem)]"
296
- style={style}
428
+ class={this.$cssMerge(
429
+ routedDialog
430
+ ? 'card bg-base-100 shadow-2xl relative w-full'
431
+ : 'card bg-base-100 shadow-2xl relative w-full max-h-[calc(100vh-2rem)]',
432
+ presentation.className,
433
+ )}
434
+ style={presentation.style}
297
435
  >
298
436
  <div class="card-body flex max-h-full min-h-0 flex-col gap-4">
299
- <div class="flex items-start gap-3 shrink-0">
437
+ <div class="flex items-center gap-3 shrink-0">
438
+ {!!showBackButton && (
439
+ <button
440
+ type="button"
441
+ class="btn btn-ghost btn-sm btn-circle shrink-0 transition-colors hover:bg-base-300"
442
+ aria-label={this.scope.locale.Back()}
443
+ title={this.scope.locale.Back()}
444
+ onClick={() => {
445
+ onBack?.();
446
+ }}
447
+ >
448
+ <ZIcon name="::arrow-back" width={18} height={18}></ZIcon>
449
+ </button>
450
+ )}
300
451
  {!!iconName && (
301
452
  <ZIcon class="text-primary mt-1 shrink-0" name={iconName} width={24}></ZIcon>
302
453
  )}
@@ -334,19 +485,44 @@ export class BehaviorAppModal extends BeanBehaviorBase<
334
485
 
335
486
  private _prepareDialogOptions(
336
487
  type: ModalType,
337
- dialogOptions?: IModalDialogOptions | IModalMessageOptions,
338
- ) {
488
+ dialogOptions?:
489
+ | IModalDialogOptions
490
+ | IModalMessageOptions
491
+ | IModalRoutedDialogPresentationOptions,
492
+ routedDialogOptions?: IModalRoutedDialogPresentationOptions,
493
+ ): IPreparedDialogOptions {
339
494
  const defaults = this.scope.config.model[type].default as
340
495
  | IModalDialogOptions
341
- | IModalMessageOptions;
342
- const options = {
343
- maxWidth: dialogOptions?.maxWidth ?? defaults.maxWidth,
496
+ | IModalMessageOptions
497
+ | IModalRoutedDialogPresentationOptions;
498
+ const maxWidth =
499
+ type === 'routedDialog'
500
+ ? this._resolveRoutedDialogMaxWidth(
501
+ defaults.maxWidth as ModalRoutedDialogMaxWidth | undefined,
502
+ routedDialogOptions?.maxWidth,
503
+ (dialogOptions as IModalRoutedDialogPresentationOptions | undefined)?.maxWidth,
504
+ )
505
+ : ((dialogOptions as IModalDialogOptions | IModalMessageOptions | undefined)?.maxWidth ??
506
+ (defaults.maxWidth as ModalWidth | undefined));
507
+ const routedPresentation = dialogOptions as IModalRoutedDialogPresentationOptions | undefined;
508
+ const routedDefaults = defaults as IModalRoutedDialogPresentationOptions;
509
+ const topGutter =
510
+ type === 'routedDialog'
511
+ ? this._resolveRoutedDialogTopGutter(
512
+ routedDefaults.topGutter,
513
+ routedDialogOptions?.topGutter,
514
+ routedPresentation?.topGutter,
515
+ )
516
+ : undefined;
517
+ const options: IPreparedDialogOptions = {
518
+ maxWidth,
344
519
  maxHeight: dialogOptions?.maxHeight ?? defaults.maxHeight,
520
+ topGutter,
345
521
  closeOnBackdrop: dialogOptions?.closeOnBackdrop ?? defaults.closeOnBackdrop,
346
522
  closeOnEscape: dialogOptions?.closeOnEscape ?? defaults.closeOnEscape,
347
523
  showCloseButton: false,
348
524
  };
349
- if (type !== 'dialog') return options;
525
+ if (type !== 'dialog' && type !== 'routedDialog') return options;
350
526
  const defaultsDialog = defaults as IModalDialogOptions;
351
527
  return {
352
528
  ...options,
@@ -354,20 +530,132 @@ export class BehaviorAppModal extends BeanBehaviorBase<
354
530
  (dialogOptions as IModalDialogOptions | undefined)?.showCloseButton ??
355
531
  defaultsDialog.showCloseButton ??
356
532
  false,
533
+ showBackButton:
534
+ type === 'routedDialog'
535
+ ? (routedPresentation?.showBackButton ??
536
+ routedDialogOptions?.showBackButton ??
537
+ routedDefaults.showBackButton ??
538
+ false)
539
+ : undefined,
357
540
  };
358
541
  }
359
542
 
360
- private _dialogStyle(dialogOptions: IModalDialogOptions) {
543
+ private _resolveRoutedDialogMaxWidth(
544
+ ...values: (ModalRoutedDialogMaxWidth | undefined)[]
545
+ ): ModalRoutedDialogMaxWidth | undefined {
546
+ return this._resolveRoutedDialogLength(values) as ModalRoutedDialogMaxWidth | undefined;
547
+ }
548
+
549
+ private _resolveRoutedDialogTopGutter(
550
+ ...values: (ModalRoutedDialogTopGutter | undefined)[]
551
+ ): ModalRoutedDialogTopGutter | undefined {
552
+ return this._resolveRoutedDialogLength(values) as ModalRoutedDialogTopGutter | undefined;
553
+ }
554
+
555
+ private _resolveRoutedDialogLength(
556
+ values: (ModalRoutedDialogMaxWidth | ModalRoutedDialogTopGutter | undefined)[],
557
+ ): ModalRoutedDialogMaxWidth | ModalRoutedDialogTopGutter | undefined {
558
+ let resolved: ModalRoutedDialogMaxWidth | ModalRoutedDialogTopGutter | undefined;
559
+ for (const value of values) {
560
+ if (value === undefined) continue;
561
+ if (!this._isResponsiveLength(value)) {
562
+ resolved = value;
563
+ continue;
564
+ }
565
+ resolved = {
566
+ ...(this._isResponsiveLength(resolved) ? resolved : { default: resolved }),
567
+ ...value,
568
+ };
569
+ }
570
+ return resolved;
571
+ }
572
+
573
+ private _isResponsiveMaxWidth(
574
+ value: ModalRoutedDialogMaxWidth | undefined,
575
+ ): value is IModalResponsiveMaxWidth {
576
+ return this._isResponsiveLength(value);
577
+ }
578
+
579
+ private _isResponsiveTopGutter(
580
+ value: ModalRoutedDialogTopGutter | undefined,
581
+ ): value is IModalResponsiveTopGutter {
582
+ return this._isResponsiveLength(value);
583
+ }
584
+
585
+ private _isResponsiveLength(
586
+ value: ModalRoutedDialogMaxWidth | ModalRoutedDialogTopGutter | undefined,
587
+ ): value is IModalResponsiveMaxWidth | IModalResponsiveTopGutter {
588
+ return !!value && typeof value === 'object';
589
+ }
590
+
591
+ private _routedDialogOuterPresentation(
592
+ dialogOptions: IPreparedDialogOptions,
593
+ ): IModalOuterPresentation {
594
+ const style = {} as Record<string, string>;
595
+ const topGutter = dialogOptions.topGutter;
596
+ if (this._isResponsiveTopGutter(topGutter)) {
597
+ this._setResponsiveTopGutter(style, 'default', topGutter.default);
598
+ this._setResponsiveTopGutter(style, 'md', topGutter.md);
599
+ this._setResponsiveTopGutter(style, 'lg', topGutter.lg);
600
+ } else if (topGutter !== undefined) {
601
+ this._setResponsiveTopGutter(style, 'default', topGutter);
602
+ }
603
+ return {
604
+ className: this.$style(responsiveTopGutterClass),
605
+ style: Object.keys(style).length > 0 ? style : undefined,
606
+ };
607
+ }
608
+
609
+ private _dialogPresentation(
610
+ dialogOptions: IPreparedDialogOptions,
611
+ routedDialog: boolean,
612
+ ): IModalCardPresentation {
361
613
  const style = {} as Record<string, string>;
362
614
  const maxWidth = dialogOptions.maxWidth;
363
615
  const maxHeight = dialogOptions.maxHeight;
364
- if (maxWidth) {
365
- style.maxWidth = typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth;
616
+ const classNames: string[] = [];
617
+ if (this._isResponsiveMaxWidth(maxWidth)) {
618
+ classNames.push(this.$style(responsiveMaxWidthClass));
619
+ this._setResponsiveMaxWidth(style, 'default', maxWidth.default);
620
+ this._setResponsiveMaxWidth(style, 'md', maxWidth.md);
621
+ this._setResponsiveMaxWidth(style, 'lg', maxWidth.lg);
622
+ } else if (maxWidth !== undefined) {
623
+ style.maxWidth = this._normalizeWidth(maxWidth);
366
624
  }
367
- if (maxHeight) {
368
- style.maxHeight = typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight;
625
+ if (routedDialog) {
626
+ classNames.push(this.$style(routedDialogMaxHeightClass));
627
+ if (maxHeight !== undefined) {
628
+ style['--zova-routed-dialog-max-height'] = this._normalizeWidth(maxHeight);
629
+ }
630
+ } else if (maxHeight !== undefined) {
631
+ style.maxHeight = this._normalizeWidth(maxHeight);
369
632
  }
370
- return Object.keys(style).length > 0 ? style : undefined;
633
+ return {
634
+ className: classNames.length > 0 ? this.$cssMerge(...classNames) : undefined,
635
+ style: Object.keys(style).length > 0 ? style : undefined,
636
+ };
637
+ }
638
+
639
+ private _setResponsiveMaxWidth(
640
+ style: Record<string, string>,
641
+ breakpoint: keyof IModalResponsiveMaxWidth,
642
+ value: ModalWidth | undefined,
643
+ ) {
644
+ if (value === undefined) return;
645
+ style[`--zova-routed-dialog-max-width-${breakpoint}`] = this._normalizeWidth(value);
646
+ }
647
+
648
+ private _setResponsiveTopGutter(
649
+ style: Record<string, string>,
650
+ breakpoint: keyof IModalResponsiveTopGutter,
651
+ value: ModalWidth | undefined,
652
+ ) {
653
+ if (value === undefined) return;
654
+ style[`--zova-routed-dialog-top-gutter-${breakpoint}`] = this._normalizeWidth(value);
655
+ }
656
+
657
+ private _normalizeWidth(value: ModalWidth) {
658
+ return typeof value === 'number' ? `${value}px` : value;
371
659
  }
372
660
 
373
661
  private _getButtonClass(type: AlertType, primary?: boolean) {