underpost 2.8.84 → 2.8.85

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 (104) hide show
  1. package/.env.development +1 -0
  2. package/.env.production +1 -0
  3. package/.env.test +1 -0
  4. package/.github/workflows/{ghpkg.yml → ghpkg.ci.yml} +1 -1
  5. package/.github/workflows/{npmpkg.yml → npmpkg.ci.yml} +1 -1
  6. package/.github/workflows/{publish.yml → publish.ci.yml} +1 -1
  7. package/.github/workflows/{pwa-microservices-template.page.yml → pwa-microservices-template-page.cd.yml} +1 -1
  8. package/.github/workflows/{pwa-microservices-template.test.yml → pwa-microservices-template-test.ci.yml} +1 -1
  9. package/.vscode/settings.json +0 -1
  10. package/README.md +45 -2
  11. package/bin/build.js +15 -5
  12. package/bin/deploy.js +17 -82
  13. package/bin/file.js +15 -8
  14. package/cli.md +65 -44
  15. package/conf.js +1 -1
  16. package/docker-compose.yml +1 -1
  17. package/manifests/deployment/dd-template-development/deployment.yaml +2 -2
  18. package/manifests/maas/gpu-diag.sh +1 -1
  19. package/package.json +4 -6
  20. package/src/api/user/user.router.js +24 -1
  21. package/src/api/user/user.service.js +1 -4
  22. package/src/cli/cluster.js +42 -27
  23. package/src/cli/deploy.js +20 -0
  24. package/src/cli/index.js +9 -0
  25. package/src/cli/monitor.js +8 -12
  26. package/src/cli/run.js +111 -6
  27. package/src/cli/ssh.js +32 -0
  28. package/src/client/Default.index.js +7 -3
  29. package/src/client/components/core/Account.js +1 -1
  30. package/src/client/components/core/Chat.js +1 -1
  31. package/src/client/components/core/CommonJs.js +24 -22
  32. package/src/client/components/core/Content.js +1 -5
  33. package/src/client/components/core/Css.js +258 -18
  34. package/src/client/components/core/CssCore.js +8 -8
  35. package/src/client/components/core/Docs.js +14 -61
  36. package/src/client/components/core/DropDown.js +137 -82
  37. package/src/client/components/core/EventsUI.js +92 -5
  38. package/src/client/components/core/LoadingAnimation.js +8 -15
  39. package/src/client/components/core/Modal.js +597 -136
  40. package/src/client/components/core/NotificationManager.js +2 -2
  41. package/src/client/components/core/ObjectLayerEngine.js +638 -0
  42. package/src/client/components/core/Panel.js +158 -34
  43. package/src/client/components/core/PanelForm.js +12 -3
  44. package/src/client/components/core/Recover.js +1 -1
  45. package/src/client/components/core/Router.js +77 -17
  46. package/src/client/components/core/SocketIo.js +3 -3
  47. package/src/client/components/core/Translate.js +6 -2
  48. package/src/client/components/core/VanillaJs.js +0 -3
  49. package/src/client/components/core/Worker.js +3 -1
  50. package/src/client/components/default/CssDefault.js +17 -3
  51. package/src/client/components/default/MenuDefault.js +264 -45
  52. package/src/client/components/default/RoutesDefault.js +6 -12
  53. package/src/client/public/default/android-chrome-144x144.png +0 -0
  54. package/src/client/public/default/android-chrome-192x192.png +0 -0
  55. package/src/client/public/default/android-chrome-256x256.png +0 -0
  56. package/src/client/public/default/android-chrome-36x36.png +0 -0
  57. package/src/client/public/default/android-chrome-48x48.png +0 -0
  58. package/src/client/public/default/android-chrome-72x72.png +0 -0
  59. package/src/client/public/default/android-chrome-96x96.png +0 -0
  60. package/src/client/public/default/apple-touch-icon-114x114-precomposed.png +0 -0
  61. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  62. package/src/client/public/default/apple-touch-icon-120x120-precomposed.png +0 -0
  63. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  64. package/src/client/public/default/apple-touch-icon-144x144-precomposed.png +0 -0
  65. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  66. package/src/client/public/default/apple-touch-icon-152x152-precomposed.png +0 -0
  67. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  68. package/src/client/public/default/apple-touch-icon-180x180-precomposed.png +0 -0
  69. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  70. package/src/client/public/default/apple-touch-icon-57x57-precomposed.png +0 -0
  71. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  72. package/src/client/public/default/apple-touch-icon-60x60-precomposed.png +0 -0
  73. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  74. package/src/client/public/default/apple-touch-icon-72x72-precomposed.png +0 -0
  75. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  76. package/src/client/public/default/apple-touch-icon-76x76-precomposed.png +0 -0
  77. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  78. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  79. package/src/client/public/default/apple-touch-icon.png +0 -0
  80. package/src/client/public/default/assets/background/dark.jpg +0 -0
  81. package/src/client/public/default/assets/background/dark.svg +557 -0
  82. package/src/client/public/default/assets/logo/base-icon.png +0 -0
  83. package/src/client/public/default/assets/logo/underpost.gif +0 -0
  84. package/src/client/public/default/assets/mailer/api-user-check.png +0 -0
  85. package/src/client/public/default/assets/mailer/api-user-invalid-token.png +0 -0
  86. package/src/client/public/default/assets/mailer/api-user-recover.png +0 -0
  87. package/src/client/public/default/favicon-16x16.png +0 -0
  88. package/src/client/public/default/favicon-32x32.png +0 -0
  89. package/src/client/public/default/favicon.ico +0 -0
  90. package/src/client/public/default/mstile-144x144.png +0 -0
  91. package/src/client/public/default/mstile-150x150.png +0 -0
  92. package/src/client/public/default/mstile-310x150.png +0 -0
  93. package/src/client/public/default/mstile-310x310.png +0 -0
  94. package/src/client/public/default/mstile-70x70.png +0 -0
  95. package/src/client/public/default/safari-pinned-tab.svg +24 -0
  96. package/src/client/ssr/body/DefaultSplashScreen.js +2 -2
  97. package/src/index.js +9 -1
  98. package/src/monitor.js +24 -0
  99. package/src/runtime/lampp/Dockerfile +30 -39
  100. package/src/runtime/lampp/Lampp.js +11 -2
  101. package/src/server/client-build-docs.js +205 -0
  102. package/src/server/client-build.js +16 -166
  103. package/src/server/conf.js +12 -5
  104. package/src/server/valkey.js +102 -41
@@ -24,7 +24,7 @@ import {
24
24
  renderStatus,
25
25
  renderCssAttr,
26
26
  } from './Css.js';
27
- import { setDocTitle } from './Router.js';
27
+ import { setDocTitle, closeModalRouteChangeEvent, handleModalViewRoute } from './Router.js';
28
28
  import { NotificationManager } from './NotificationManager.js';
29
29
  import { EventsUI } from './EventsUI.js';
30
30
  import { Translate } from './Translate.js';
@@ -39,6 +39,7 @@ const logger = loggerFactory(import.meta);
39
39
 
40
40
  const Modal = {
41
41
  Data: {},
42
+
42
43
  Render: async function (
43
44
  options = {
44
45
  id: '',
@@ -86,14 +87,14 @@ const Modal = {
86
87
  onBarUiOpen: {},
87
88
  onBarUiClose: {},
88
89
  onHome: {},
90
+ homeModals: options.homeModals ? options.homeModals : [],
89
91
  query: options.query ? `${window.location.search}` : undefined,
90
92
  };
91
- const setCenterRestore = () => {
92
- const ResponsiveData = Responsive.getResponsiveData();
93
- top = `${ResponsiveData.height / 2 - height / 2}px`;
94
- left = `${ResponsiveData.width / 2 - width / 2}px`;
95
- };
96
- if (idModal !== 'main-body') setCenterRestore();
93
+
94
+ if (idModal !== 'main-body' && options.mode !== 'view') {
95
+ top = `${window.innerHeight / 2 - height / 2}px`;
96
+ left = `${window.innerWidth / 2 - width / 2}px`;
97
+ }
97
98
  if (options && 'mode' in options) {
98
99
  this.Data[idModal][options.mode] = {};
99
100
  switch (options.mode) {
@@ -104,6 +105,7 @@ const Modal = {
104
105
  options.style = {
105
106
  ...options.style,
106
107
  'min-width': `${minWidth}px`,
108
+ width: '100%',
107
109
  };
108
110
 
109
111
  if (this.mobileModal()) {
@@ -124,25 +126,84 @@ const Modal = {
124
126
  };
125
127
  Responsive.Event[`view-${idModal}`]();
126
128
 
127
- // Router
128
- if (options.route)
129
- (() => {
130
- let path = window.location.pathname;
131
- if (path !== '/' && path[path.length - 1] === '/') path = path.slice(0, -1);
132
- const proxyPath = getProxyPath();
133
- const newPath = `${proxyPath}${options.route}`;
134
- if (path !== newPath) {
135
- // console.warn('SET MODAL URI', newPath);
136
- setPath(`${newPath}`); // ${location.search}
137
- setDocTitle({ ...options.RouterInstance, route: options.route });
138
- }
139
- })();
129
+ // Handle view mode modal route
130
+ if (options.route) {
131
+ handleModalViewRoute({
132
+ route: options.route,
133
+ RouterInstance: options.RouterInstance,
134
+ });
135
+ }
140
136
 
141
137
  break;
142
138
  case 'slide-menu':
143
139
  case 'slide-menu-right':
144
140
  case 'slide-menu-left':
145
141
  (async () => {
142
+ if (!options.slideMenuTopBarBannerFix) {
143
+ options.slideMenuTopBarBannerFix = async () => {
144
+ let style = html``;
145
+ if (options.barMode === 'top-bottom-bar') {
146
+ style = html`<style>
147
+ .default-slide-menu-top-bar-fix-logo-container {
148
+ width: 50px;
149
+ height: 50px;
150
+ }
151
+ .default-slide-menu-top-bar-fix-logo {
152
+ width: 40px;
153
+ height: 40px;
154
+ padding: 5px;
155
+ }
156
+ .default-slide-menu-top-bar-fix-title-container-text {
157
+ font-size: 26px;
158
+ top: 8px;
159
+ color: ${darkTheme ? '#ffffff' : '#000000'};
160
+ }
161
+ </style>`;
162
+ } else {
163
+ style = html`<style>
164
+ .default-slide-menu-top-bar-fix-logo-container {
165
+ width: 100px;
166
+ height: 100px;
167
+ }
168
+ .default-slide-menu-top-bar-fix-logo {
169
+ width: 50px;
170
+ height: 50px;
171
+ padding: 24px;
172
+ }
173
+ .default-slide-menu-top-bar-fix-title-container-text {
174
+ font-size: 30px;
175
+ top: 30px;
176
+ color: ${darkTheme ? '#ffffff' : '#000000'};
177
+ }
178
+ </style>`;
179
+ }
180
+ setTimeout(() => {
181
+ if (s(`.top-bar-app-icon`) && s(`.top-bar-app-icon`).src) {
182
+ s(`.default-slide-menu-top-bar-fix-logo`).src = s(`.top-bar-app-icon`).src;
183
+ if (s(`.top-bar-app-icon`).classList.contains('negative-color'))
184
+ s(`.default-slide-menu-top-bar-fix-logo`).classList.add('negative-color');
185
+ htmls(
186
+ `.default-slide-menu-top-bar-fix-title-container`,
187
+ html`
188
+ <div class="inl default-slide-menu-top-bar-fix-title-container-text">
189
+ ${options.RouterInstance.NameApp}
190
+ </div>
191
+ `,
192
+ );
193
+ } else
194
+ htmls(
195
+ `.default-slide-menu-top-bar-fix-logo-container`,
196
+ html`<div class="abs center">${s(`.action-btn-app-icon-render`).innerHTML}</div>`,
197
+ );
198
+ });
199
+
200
+ return html`${style}
201
+ <div class="in fll default-slide-menu-top-bar-fix-logo-container">
202
+ <img class="default-slide-menu-top-bar-fix-logo in fll" />
203
+ </div>
204
+ <div class="in fll default-slide-menu-top-bar-fix-title-container"></div>`;
205
+ };
206
+ }
146
207
  const { barConfig } = options;
147
208
  options.style = {
148
209
  position: 'absolute',
@@ -247,8 +308,17 @@ const Modal = {
247
308
  style="top: ${options.heightTopBar + 50}px; z-index: 9; ${true ||
248
309
  (options.mode && options.mode.match('right'))
249
310
  ? 'right'
250
- : 'left'}: 50px; width: 50px; height: 100px; transition: .3s"
311
+ : 'left'}: 50px; width: 50px; height: 150px; transition: .3s"
251
312
  >
313
+ <div
314
+ class="abs main-body-btn main-body-btn-ui"
315
+ style="top: 0px; ${true || (options.mode && options.mode.match('right')) ? 'right' : 'left'}: 0px"
316
+ >
317
+ <div class="abs center">
318
+ <i class="fas fa-caret-down main-body-btn-ui-open hide"></i>
319
+ <i class="fas fa-caret-up main-body-btn-ui-close"></i>
320
+ </div>
321
+ </div>
252
322
  <div
253
323
  class="abs main-body-btn main-body-btn-menu"
254
324
  style="top: 50px; ${true || (options.mode && options.mode.match('right'))
@@ -261,15 +331,18 @@ const Modal = {
261
331
  </div>
262
332
  </div>
263
333
  <div
264
- class="abs main-body-btn main-body-btn-ui"
265
- style="top: 0px; ${true || (options.mode && options.mode.match('right')) ? 'right' : 'left'}: 0px"
334
+ class="abs main-body-btn main-body-btn-bar-custom ${options?.slideMenuTopBarBannerFix
335
+ ? ''
336
+ : 'hide'}"
337
+ style="top: 100px; ${true || (options.mode && options.mode.match('right'))
338
+ ? 'right'
339
+ : 'left'}: 0px"
266
340
  >
267
341
  <div class="abs center">
268
- <i class="fas fa-caret-down main-body-btn-ui-open hide"></i>
269
- <i class="fas fa-caret-up main-body-btn-ui-close"></i>
342
+ <i class="fa-solid fa-magnifying-glass main-body-btn-ui-bar-custom-open"></i>
343
+ <i class="fa-solid fa-home hide main-body-btn-ui-bar-custom-close"></i>
270
344
  </div>
271
345
  </div>
272
- <div class="main-body-btn-container-custom"></div>
273
346
  </div>
274
347
  `,
275
348
  );
@@ -278,6 +351,22 @@ const Modal = {
278
351
  Modal.actionBtnCenter();
279
352
  };
280
353
 
354
+ s(`.main-body-btn-bar-custom`).onclick = () => {
355
+ if (s(`.main-body-btn-ui-close`).classList.contains('hide')) {
356
+ s(`.main-body-btn-ui`).click();
357
+ }
358
+ if (s(`.main-body-btn-ui-bar-custom-open`).classList.contains('hide')) {
359
+ s(`.main-body-btn-ui-bar-custom-open`).classList.remove('hide');
360
+ s(`.main-body-btn-ui-bar-custom-close`).classList.add('hide');
361
+ s(`.slide-menu-top-bar-fix`).style.top = '0px';
362
+ } else {
363
+ s(`.main-body-btn-ui-bar-custom-open`).classList.add('hide');
364
+ s(`.main-body-btn-ui-bar-custom-close`).classList.remove('hide');
365
+ s(`.slide-menu-top-bar-fix`).style.top = '-100px';
366
+ s(`.top-bar-search-box-container`).click();
367
+ }
368
+ };
369
+
281
370
  let _heightTopBar, _heightBottomBar, _topMenu;
282
371
  s(`.main-body-btn-ui`).onclick = () => {
283
372
  if (s(`.main-body-btn-ui-open`).classList.contains('hide')) {
@@ -323,6 +412,7 @@ const Modal = {
323
412
  );
324
413
  }
325
414
  };
415
+ Modal.setTopBannerLink();
326
416
  });
327
417
 
328
418
  const inputSearchBoxId = `top-bar-search-box`;
@@ -407,9 +497,14 @@ const Modal = {
407
497
  })}
408
498
  </div>
409
499
  </div>
410
- ${options?.slideMenuTopBarFix
411
- ? html`<div class="abs modal slide-menu-top-bar-fix" style="height: ${options.heightTopBar}px">
412
- ${await options.slideMenuTopBarFix()}
500
+ ${options?.slideMenuTopBarBannerFix
501
+ ? html`<div
502
+ class="abs modal slide-menu-top-bar-fix"
503
+ style="height: ${options.heightTopBar}px; top: 0px"
504
+ >
505
+ <a class="a-link-top-banner">
506
+ <div class="inl">${await options.slideMenuTopBarBannerFix()}</div></a
507
+ >
413
508
  </div>`
414
509
  : ''}
415
510
  </div>`,
@@ -434,8 +529,14 @@ const Modal = {
434
529
  rules: [] /*{ type: 'isEmpty' }, { type: 'isEmail' }*/,
435
530
  },
436
531
  ];
437
- let hoverHistBox = false;
438
- let hoverInputBox = false;
532
+ // Reusable hover/focus controller for search history panel
533
+ let unbindDocSearch = null;
534
+ const hoverFocusCtl = EventsUI.HoverFocusController({
535
+ inputSelector: `.top-bar-search-box-container`,
536
+ panelSelector: `.${id}`,
537
+ activeElementId: inputSearchBoxId,
538
+ onDismiss: () => dismissSearchBox(),
539
+ });
439
540
  let currentKeyBoardSearchBoxIndex = 0;
440
541
  let results = [];
441
542
  let historySearchBox = [];
@@ -592,17 +693,29 @@ const Modal = {
592
693
  const isSearchBoxActiveElement = isActiveElement(inputSearchBoxId);
593
694
  checkHistoryBoxTitleStatus();
594
695
  checkShortcutContainerInfoEnabled();
595
- if (!isSearchBoxActiveElement && !hoverHistBox && !hoverInputBox) {
696
+ if (!isSearchBoxActiveElement && !hoverFocusCtl.shouldStay()) {
596
697
  Modal.removeModal(searchBoxHistoryId);
597
698
  return;
598
699
  }
599
- setTimeout(() => getResultSearchBox(validatorData));
700
+ setTimeout(() => {
701
+ getResultSearchBox(validatorData);
702
+
703
+ if (
704
+ s(`.slide-menu-top-bar-fix`) &&
705
+ !s(`.main-body-btn-ui-bar-custom-open`).classList.contains('hide')
706
+ ) {
707
+ s(`.main-body-btn-bar-custom`).click();
708
+ }
709
+ });
600
710
  };
601
711
 
602
712
  const getDefaultSearchBoxSelector = () => `.search-result-btn-${currentKeyBoardSearchBoxIndex}`;
603
713
 
604
714
  const updateSearchBoxValue = (selector) => {
605
715
  if (!selector) selector = getDefaultSearchBoxSelector();
716
+ // check exist childNodes
717
+ if (!s(selector) || !s(selector).hasChildNodes()) return;
718
+
606
719
  if (s(selector).childNodes) {
607
720
  if (
608
721
  s(selector).childNodes[s(selector).childNodes.length - 1] &&
@@ -626,6 +739,10 @@ const Modal = {
626
739
 
627
740
  const setSearchValue = (selector) => {
628
741
  if (!selector) selector = getDefaultSearchBoxSelector();
742
+
743
+ // check exist childNodes
744
+ if (!s(selector) || !s(selector).hasChildNodes()) return;
745
+
629
746
  historySearchBox = historySearchBox.filter(
630
747
  (h) => h.routerId !== results[currentKeyBoardSearchBoxIndex].routerId,
631
748
  );
@@ -676,27 +793,19 @@ const Modal = {
676
793
  barMode: options.barMode,
677
794
  });
678
795
 
679
- const titleNode = s(`.title-modal-${id}`).cloneNode(true);
680
- s(`.title-modal-${id}`).remove();
681
- s(`.btn-bar-modal-container-render-${id}`).classList.add('in');
682
- s(`.btn-bar-modal-container-render-${id}`).classList.add('fll');
683
- s(`.btn-bar-modal-container-render-${id}`).appendChild(titleNode);
796
+ // Bind hover/focus and click-outside to dismiss
797
+ hoverFocusCtl.bind();
798
+ unbindDocSearch = EventsUI.bindDismissOnDocumentClick({
799
+ shouldStay: hoverFocusCtl.shouldStay,
800
+ onDismiss: () => dismissSearchBox(),
801
+ anchors: [`.top-bar-search-box-container`, `.${id}`],
802
+ });
803
+ // Ensure cleanup when modal closes
804
+ Modal.Data[id].onCloseListener[`unbind-doc-${id}`] = () => unbindDocSearch && unbindDocSearch();
684
805
 
685
- prepend(`.btn-bar-modal-container-${id}`, html`<div class="hide">${inputInfoNode.outerHTML}</div>`);
806
+ Modal.MoveTitleToBar(id);
686
807
 
687
- s(`.top-bar-search-box-container`).onmouseover = () => {
688
- hoverInputBox = true;
689
- };
690
- s(`.top-bar-search-box-container`).onmouseout = () => {
691
- hoverInputBox = false;
692
- };
693
- s(`.${id}`).onmouseover = () => {
694
- hoverHistBox = true;
695
- };
696
- s(`.${id}`).onmouseout = () => {
697
- hoverHistBox = false;
698
- s(`.${inputSearchBoxId}`).focus();
699
- };
808
+ prepend(`.btn-bar-modal-container-${id}`, html`<div class="hide">${inputInfoNode.outerHTML}</div>`);
700
809
  }
701
810
  };
702
811
 
@@ -708,14 +817,23 @@ const Modal = {
708
817
  searchBoxHistoryOpen();
709
818
  searchBoxCallBack(formDataInfoNode[0]);
710
819
  };
711
- s('.top-bar-search-box').onblur = () => {
712
- if (!hoverHistBox && !hoverInputBox && !isActiveElement(inputSearchBoxId)) {
713
- Modal.removeModal(searchBoxHistoryId);
820
+
821
+ const dismissSearchBox = () => {
822
+ if (unbindDocSearch) {
823
+ try {
824
+ unbindDocSearch();
825
+ } catch (e) {}
714
826
  }
827
+ Modal.removeModal(searchBoxHistoryId);
828
+ };
829
+ s('.top-bar-search-box').onblur = () => {
830
+ hoverFocusCtl.checkDismiss();
715
831
  };
716
832
  EventsUI.onClick(`.top-bar-search-box-container`, () => {
717
833
  searchBoxHistoryOpen();
718
834
  searchBoxCallBack(formDataInfoNode[0]);
835
+ const inputEl = s(`.${inputSearchBoxId}`);
836
+ if (inputEl && inputEl.focus) inputEl.focus();
719
837
  });
720
838
 
721
839
  const timePressDelay = 100;
@@ -881,6 +999,9 @@ const Modal = {
881
999
  ],
882
1000
  eventCallBack: () => {
883
1001
  if (s(`.top-bar-search-box`)) {
1002
+ if (s(`.main-body-btn-ui-close`).classList.contains('hide')) {
1003
+ s(`.main-body-btn-ui-open`).click();
1004
+ }
884
1005
  s(`.top-bar-search-box`).blur();
885
1006
  s(`.top-bar-search-box`).focus();
886
1007
  s(`.top-bar-search-box`).select();
@@ -897,8 +1018,10 @@ const Modal = {
897
1018
  barConfig.buttons.menu.disabled = true;
898
1019
  barConfig.buttons.close.disabled = true;
899
1020
  const id = 'bottom-bar';
900
- if (options && options.homeModals && !options.homeModals.includes(id)) options.homeModals.push(id);
901
- else options.homeModals = [id];
1021
+ if (!this.Data[idModal].homeModals) this.Data[idModal].homeModals = [];
1022
+ if (!this.Data[idModal].homeModals.includes(id)) {
1023
+ this.Data[idModal].homeModals.push(id);
1024
+ }
902
1025
  const html = async () => html`
903
1026
  <style>
904
1027
  .top-bar-search-box-container {
@@ -1015,7 +1138,7 @@ const Modal = {
1015
1138
  await Modal.onHomeRouterEvent();
1016
1139
  Object.keys(this.Data[idModal].onHome).map((keyListener) => this.Data[idModal].onHome[keyListener]());
1017
1140
  });
1018
- EventsUI.onClick(`.action-btn-app-icon`, () => Modal.onHomeRouterEvent());
1141
+ EventsUI.onClick(`.action-btn-app-icon`, () => s(`.action-btn-home`).click());
1019
1142
  Keyboard.instanceMultiPressKey({
1020
1143
  id: 'input-shortcut-global-escape',
1021
1144
  keys: ['Escape'],
@@ -1026,11 +1149,18 @@ const Modal = {
1026
1149
  }
1027
1150
 
1028
1151
  {
1029
- ThemeEvents['action-btn-theme'] = () => {
1152
+ ThemeEvents['action-btn-theme'] = async () => {
1030
1153
  htmls(
1031
1154
  `.action-btn-theme-render`,
1032
1155
  html` ${darkTheme ? html` <i class="fas fa-moon"></i>` : html`<i class="far fa-sun"></i>`}`,
1033
1156
  );
1157
+ if (s(`.slide-menu-top-bar-fix`)) {
1158
+ htmls(
1159
+ `.slide-menu-top-bar-fix`,
1160
+ html`<a class="a-link-top-banner">${await options.slideMenuTopBarBannerFix()}</a>`,
1161
+ );
1162
+ Modal.setTopBannerLink();
1163
+ }
1034
1164
  };
1035
1165
  ThemeEvents['action-btn-theme']();
1036
1166
 
@@ -1048,13 +1178,80 @@ const Modal = {
1048
1178
 
1049
1179
  {
1050
1180
  htmls(`.action-btn-lang-render`, html` ${s('html').lang}`);
1051
- EventsUI.onClick(`.action-btn-lang`, () => {
1052
- let lang = 'en';
1053
- if (s('html').lang === 'en') lang = 'es';
1054
- if (s(`.dropdown-option-${lang}`))
1055
- DropDown.Tokens['settings-lang'].onClickEvents[`dropdown-option-${lang}`]();
1056
- else Translate.renderLang(lang);
1057
- });
1181
+ // old method
1182
+ // EventsUI.onClick(`.action-btn-lang`, () => {
1183
+ // let lang = 'en';
1184
+ // if (s('html').lang === 'en') lang = 'es';
1185
+ // if (s(`.dropdown-option-${lang}`))
1186
+ // DropDown.Tokens['settings-lang'].onClickEvents[`dropdown-option-${lang}`]();
1187
+ // else Translate.renderLang(lang);
1188
+ // });
1189
+
1190
+ // Open lightweight empty modal on language button, with shared dismiss logic
1191
+ EventsUI.onClick(
1192
+ `.action-btn-lang`,
1193
+ async () => {
1194
+ const id = 'action-btn-lang-modal';
1195
+ if (s(`.${id}`)) {
1196
+ return s(`.btn-close-${id}`).click();
1197
+ }
1198
+ const { barConfig } = await Themes[Css.currentTheme]();
1199
+ barConfig.buttons.maximize.disabled = true;
1200
+ barConfig.buttons.minimize.disabled = true;
1201
+ barConfig.buttons.restore.disabled = true;
1202
+ barConfig.buttons.menu.disabled = true;
1203
+ barConfig.buttons.close.disabled = false;
1204
+ await Modal.Render({
1205
+ id,
1206
+ barConfig,
1207
+ title: html`${renderViewTitle({
1208
+ icon: html`<i class="fas fa-language mini-title"></i>`,
1209
+ text: Translate.Render('select lang'),
1210
+ })}`,
1211
+ html: async () => html`${await Translate.RenderSetting('action-drop-modal' + id)}`,
1212
+ titleClass: 'mini-title',
1213
+ style: {
1214
+ resize: 'none',
1215
+ width: '100% !important',
1216
+ height: '350px !important',
1217
+ 'max-width': '350px !important',
1218
+ 'z-index': 7,
1219
+ },
1220
+ dragDisabled: true,
1221
+ maximize: true,
1222
+ heightBottomBar: 0,
1223
+ heightTopBar: originHeightTopBar,
1224
+ barMode: options.barMode,
1225
+ });
1226
+
1227
+ // Move title inside the bar container to align with control buttons
1228
+ Modal.MoveTitleToBar(id);
1229
+
1230
+ // Position the language selection modal relative to the language button
1231
+ Modal.positionRelativeToAnchor({
1232
+ modalSelector: `.${id}`,
1233
+ anchorSelector: '.action-btn-lang',
1234
+ align: 'right',
1235
+ offset: { x: 0, y: 6 },
1236
+ autoVertical: true,
1237
+ });
1238
+
1239
+ // Hover/focus controller uses the button as input anchor
1240
+ const hoverFocusCtl = EventsUI.HoverFocusController({
1241
+ inputSelector: `.action-btn-lang`,
1242
+ panelSelector: `.${id}`,
1243
+ onDismiss: () => Modal.removeModal(id),
1244
+ });
1245
+ hoverFocusCtl.bind();
1246
+ const unbindDoc = EventsUI.bindDismissOnDocumentClick({
1247
+ shouldStay: hoverFocusCtl.shouldStay,
1248
+ onDismiss: () => Modal.removeModal(id),
1249
+ anchors: [`.action-btn-lang`, `.${id}`],
1250
+ });
1251
+ Modal.Data[id].onCloseListener[`unbind-doc-${id}`] = () => unbindDoc();
1252
+ },
1253
+ { context: 'modal', noGate: true, noLoading: true },
1254
+ );
1058
1255
  }
1059
1256
 
1060
1257
  {
@@ -1348,19 +1545,18 @@ const Modal = {
1348
1545
  this.onHomeRouterEvent = async () => {
1349
1546
  for (const keyModal of Object.keys(this.Data)) {
1350
1547
  if (
1351
- ![idModal, 'main-body-top', 'main-body']
1352
- .concat(options?.homeModals ? options.homeModals : [])
1353
- .includes(keyModal)
1548
+ ![idModal, 'main-body-top', 'main-body'].concat(this.Data[idModal]?.homeModals || []).includes(keyModal)
1354
1549
  )
1355
- s(`.btn-close-${keyModal}`).click();
1550
+ if (s(`.btn-close-${keyModal}`)) s(`.btn-close-${keyModal}`).click();
1356
1551
  backMenuButtonEvent();
1357
1552
  }
1358
- s(`.btn-close-modal-menu`).click();
1553
+ if (s(`.btn-close-modal-menu`)) s(`.btn-close-modal-menu`).click();
1359
1554
  setPath(getProxyPath());
1360
- setDocTitle({ ...options.RouterInstance, route: '' });
1555
+ setDocTitle();
1361
1556
  };
1362
1557
  s(`.main-btn-home`).onclick = async () => {
1363
- await this.onHomeRouterEvent();
1558
+ // await this.onHomeRouterEvent();
1559
+ s(`.action-btn-home`).click();
1364
1560
  };
1365
1561
  EventsUI.onClick(`.btn-icon-menu-back`, backMenuButtonEvent);
1366
1562
  EventsUI.onClick(`.btn-icon-menu-mode`, () => {
@@ -1429,42 +1625,139 @@ const Modal = {
1429
1625
  default:
1430
1626
  break;
1431
1627
  }
1628
+ // Track drag position for consistency
1629
+ let dragPosition = { x: 0, y: 0 };
1630
+
1631
+ // Initialize drag options with proper bounds and smooth transitions
1432
1632
  let dragOptions = {
1433
- // disabled: true,
1434
- handle,
1435
- onDragStart: (data) => {
1436
- if (!s(`.${idModal}`)) return;
1437
- // logger.info('Dragging started', data);
1438
- s(`.${idModal}`).style.transition = null;
1633
+ handle: handle,
1634
+ bounds: {
1635
+ top: 0,
1636
+ left: 0,
1637
+ right: 0,
1638
+ bottom: 0,
1639
+ },
1640
+ preventDefault: true,
1641
+ position: { x: 0, y: 0 },
1642
+ onDragStart: () => {
1643
+ const modal = s(`.${idModal}`);
1644
+ if (!modal) return false; // Prevent drag if modal not found
1645
+
1646
+ // Store current position
1647
+ const computedStyle = window.getComputedStyle(modal);
1648
+ const matrix = new DOMMatrixReadOnly(computedStyle.transform);
1649
+ dragPosition = {
1650
+ x: matrix.m41 || 0,
1651
+ y: matrix.m42 || 0,
1652
+ };
1653
+
1654
+ modal.style.transition = 'none';
1655
+ modal.style.willChange = 'transform';
1656
+ return true; // Allow drag to start
1439
1657
  },
1440
1658
  onDrag: (data) => {
1441
- if (!s(`.${idModal}`)) return;
1442
- // logger.info('Dragging', data);
1659
+ // Update position based on drag delta
1660
+ dragPosition = { x: data.x, y: data.y };
1443
1661
  },
1444
- onDragEnd: (data) => {
1445
- if (!s(`.${idModal}`)) return;
1446
- // logger.info('Dragging stopped', data);
1447
- s(`.${idModal}`).style.transition = transition;
1448
- Object.keys(this.Data[idModal].onDragEndListener).map((keyListener) =>
1449
- this.Data[idModal].onDragEndListener[keyListener](),
1450
- );
1662
+ onDragEnd: () => {
1663
+ const modal = s(`.${idModal}`);
1664
+ if (!modal) return;
1665
+
1666
+ modal.style.willChange = '';
1667
+ modal.style.transition = transition;
1668
+
1669
+ // Update drag instance with current position
1670
+ if (dragInstance) {
1671
+ dragInstance.updateOptions({
1672
+ position: dragPosition,
1673
+ });
1674
+ }
1675
+
1676
+ // Notify listeners
1677
+ Object.keys(this.Data[idModal].onDragEndListener || {}).forEach((keyListener) => {
1678
+ this.Data[idModal].onDragEndListener[keyListener]?.();
1679
+ });
1451
1680
  },
1452
1681
  };
1453
- let dragInstance;
1454
- // new Draggable(s(`.${idModal}`), { disabled: true });
1455
- const setDragInstance = () => (options?.dragDisabled ? null : new Draggable(s(`.${idModal}`), dragOptions));
1682
+
1683
+ let dragInstance = null;
1684
+
1685
+ // Initialize or update drag instance
1686
+ const setDragInstance = () => {
1687
+ if (options?.dragDisabled) {
1688
+ if (dragInstance) {
1689
+ dragInstance.destroy();
1690
+ dragInstance = null;
1691
+ }
1692
+ return null;
1693
+ }
1694
+
1695
+ const modal = s(`.${idModal}`);
1696
+ if (!modal) {
1697
+ console.warn(`Modal element .${idModal} not found for drag initialization`);
1698
+ return null;
1699
+ }
1700
+
1701
+ // Ensure the modal has position: absolute for proper dragging
1702
+ if (window.getComputedStyle(modal).position !== 'absolute') {
1703
+ modal.style.position = 'absolute';
1704
+ }
1705
+
1706
+ // Clean up existing instance
1707
+ if (dragInstance) {
1708
+ dragInstance.destroy();
1709
+ }
1710
+
1711
+ try {
1712
+ // Create new instance with updated options
1713
+ dragInstance = new Draggable(modal, dragOptions);
1714
+ return dragInstance;
1715
+ } catch (error) {
1716
+ console.error('Failed to initialize draggable:', error);
1717
+ return null;
1718
+ }
1719
+ };
1720
+
1721
+ // Expose method to update drag options
1456
1722
  this.Data[idModal].setDragInstance = (updateDragOptions) => {
1457
- dragOptions = {
1458
- ...dragOptions,
1459
- ...updateDragOptions,
1460
- };
1723
+ if (updateDragOptions) {
1724
+ dragOptions = { ...dragOptions, ...updateDragOptions };
1725
+ }
1461
1726
  dragInstance = setDragInstance();
1462
1727
  this.Data[idModal].dragInstance = dragInstance;
1463
1728
  this.Data[idModal].dragOptions = dragOptions;
1464
1729
  };
1465
- s(`.${idModal}`).style.transition = '0.15s';
1466
- setTimeout(() => (s(`.${idModal}`).style.opacity = '1'));
1467
- setTimeout(() => (s(`.${idModal}`).style.transition = transition), 150);
1730
+ // Initialize modal with proper transitions
1731
+ const modal = s(`.${idModal}`);
1732
+ if (modal) {
1733
+ // Initial state
1734
+ modal.style.transition = 'opacity 0.15s ease, transform 0.3s ease';
1735
+ modal.style.opacity = '0';
1736
+
1737
+ // Trigger fade-in after a small delay to allow initial render
1738
+ requestAnimationFrame(() => {
1739
+ if (!modal) return;
1740
+ modal.style.opacity = '1';
1741
+
1742
+ // Set final transition after initial animation completes
1743
+ setTimeout(() => {
1744
+ if (modal) {
1745
+ modal.style.transition = transition;
1746
+
1747
+ // Initialize drag after transitions are set
1748
+ if (!options.dragDisabled) {
1749
+ setDragInstance();
1750
+ if (!options.mode) {
1751
+ dragInstance.updateOptions({
1752
+ position: { x: 0, y: 0 },
1753
+ disabled: false, // Ensure drag is enabled after restore
1754
+ });
1755
+ }
1756
+ }
1757
+ }
1758
+ }, 150);
1759
+ });
1760
+ }
1468
1761
 
1469
1762
  const btnCloseEvent = () => {
1470
1763
  Object.keys(this.Data[idModal].onCloseListener).map((keyListener) =>
@@ -1480,62 +1773,122 @@ const Modal = {
1480
1773
  setTimeout(() => {
1481
1774
  if (!s(`.${idModal}`)) return;
1482
1775
  this.removeModal(idModal);
1483
- // Router
1484
- if (options.route)
1485
- (() => {
1486
- let path = window.location.pathname;
1487
- if (path[path.length - 1] !== '/') path = `${path}/`;
1488
- let newPath = `${getProxyPath()}`;
1489
- if (path !== newPath) {
1490
- for (const subIdModal of Object.keys(this.Data).reverse()) {
1491
- if (this.Data[subIdModal].options.route) {
1492
- newPath = `${newPath}${this.Data[subIdModal].options.route}`;
1493
- // console.warn('SET MODAL URI', newPath);
1494
- setPath(newPath);
1495
- this.setTopModalCallback(subIdModal);
1496
- return setDocTitle({ ...options.RouterInstance, route: this.Data[subIdModal].options.route });
1497
- }
1498
- }
1499
- // console.warn('SET MODAL URI', newPath);
1500
- setPath(`${newPath}${Modal.homeCid ? `?cid=${Modal.homeCid}` : ''}`);
1501
- return setDocTitle({ ...options.RouterInstance, route: '' });
1502
- }
1503
- })();
1776
+ // Handle modal route change
1777
+ if (options.route) {
1778
+ closeModalRouteChangeEvent({
1779
+ route: options.route,
1780
+ RouterInstance: options.RouterInstance,
1781
+ homeCid: Modal.homeCid,
1782
+ });
1783
+ }
1504
1784
  }, 300);
1505
1785
  };
1506
1786
  s(`.btn-close-${idModal}`).onclick = btnCloseEvent;
1507
1787
 
1788
+ // Minimize button handler
1508
1789
  s(`.btn-minimize-${idModal}`).onclick = () => {
1509
- if (options.slideMenu) delete this.Data[idModal].slideMenu;
1510
- s(`.${idModal}`).style.transition = '0.3s';
1790
+ const modal = s(`.${idModal}`);
1791
+ if (!modal) return;
1792
+
1793
+ if (options.slideMenu) {
1794
+ delete this.Data[idModal].slideMenu;
1795
+ }
1796
+
1797
+ // Keep drag enabled when minimized
1798
+ if (dragInstance) {
1799
+ dragInstance.updateOptions({ disabled: false });
1800
+ }
1801
+
1802
+ // Set up transition
1803
+ modal.style.transition = 'height 0.3s ease, transform 0.3s ease';
1804
+
1805
+ // Update button states
1511
1806
  s(`.btn-minimize-${idModal}`).style.display = 'none';
1512
1807
  s(`.btn-maximize-${idModal}`).style.display = null;
1513
1808
  s(`.btn-restore-${idModal}`).style.display = null;
1514
- s(`.${idModal}`).style.height = `${s(`.bar-default-modal-${idModal}`).clientHeight}px`;
1515
- setTimeout(() => (s(`.${idModal}`).style.transition = transition), 300);
1809
+
1810
+ // Collapse to header height
1811
+ const header = s(`.bar-default-modal-${idModal}`);
1812
+ if (header) {
1813
+ modal.style.height = `${header.clientHeight}px`;
1814
+ modal.style.overflow = 'hidden';
1815
+ }
1816
+
1817
+ // Restore transition after animation
1818
+ setTimeout(() => {
1819
+ if (modal) {
1820
+ modal.style.transition = transition;
1821
+ }
1822
+ }, 300);
1516
1823
  };
1824
+ // Restore button handler
1517
1825
  s(`.btn-restore-${idModal}`).onclick = () => {
1518
- if (options.slideMenu) delete this.Data[idModal].slideMenu;
1519
- s(`.${idModal}`).style.transition = '0.3s';
1826
+ const modal = s(`.${idModal}`);
1827
+ if (!modal) return;
1828
+
1829
+ if (options.slideMenu) {
1830
+ delete this.Data[idModal].slideMenu;
1831
+ }
1832
+
1833
+ // Re-enable dragging
1834
+ if (dragInstance) {
1835
+ dragInstance.updateOptions({ disabled: false });
1836
+ }
1837
+
1838
+ // Set up transition
1839
+ modal.style.transition = 'all 0.3s ease';
1840
+
1841
+ // Update button states
1520
1842
  s(`.btn-restore-${idModal}`).style.display = 'none';
1521
1843
  s(`.btn-minimize-${idModal}`).style.display = null;
1522
1844
  s(`.btn-maximize-${idModal}`).style.display = null;
1523
- s(`.${idModal}`).style.transform = null;
1524
- s(`.${idModal}`).style.height = null;
1525
- s(`.${idModal}`).style.width = null;
1526
- setCenterRestore();
1527
- s(`.${idModal}`).style.top = top;
1528
- s(`.${idModal}`).style.left = left;
1529
- dragInstance = setDragInstance();
1530
- setTimeout(() => (s(`.${idModal}`).style.transition = transition), 300);
1845
+
1846
+ // Restore original dimensions and position
1847
+ modal.style.transform = '';
1848
+ modal.style.height = '';
1849
+ left = 0;
1850
+ width = 300;
1851
+ modal.style.left = `${left}px`;
1852
+ modal.style.width = `${width}px`;
1853
+ modal.style.overflow = '';
1854
+
1855
+ // Reset drag position
1856
+ dragPosition = { x: 0, y: 0 };
1857
+
1858
+ // Set new position
1859
+ modal.style.transform = `translate(0, 0)`;
1860
+
1861
+ // Adjust top position based on top bar visibility
1862
+ const heightDefaultTopBar = 40; // Default top bar height if not specified
1863
+ s(`.${idModal}`).style.top = s(`.main-body-btn-ui-close`).classList.contains('hide')
1864
+ ? `0px`
1865
+ : `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`;
1866
+
1867
+ // Re-enable drag after restore
1868
+ if (dragInstance) {
1869
+ dragInstance.updateOptions({
1870
+ position: { x: 0, y: 0 },
1871
+ disabled: false, // Ensure drag is enabled after restore
1872
+ });
1873
+ }
1874
+ setTimeout(() => (s(`.${idModal}`) ? (s(`.${idModal}`).style.transition = transition) : null), 300);
1531
1875
  };
1532
1876
  s(`.btn-maximize-${idModal}`).onclick = () => {
1533
- s(`.${idModal}`).style.transition = '0.3s';
1534
- setTimeout(() => (s(`.${idModal}`).style.transition = transition), 300);
1877
+ const modal = s(`.${idModal}`);
1878
+ if (!modal) return;
1879
+
1880
+ // Disable drag when maximizing
1881
+ if (dragInstance) {
1882
+ dragInstance.updateOptions({ disabled: true });
1883
+ }
1884
+
1885
+ modal.style.transition = '0.3s';
1886
+ setTimeout(() => (modal ? (modal.style.transition = transition) : null), 300);
1887
+
1535
1888
  s(`.btn-maximize-${idModal}`).style.display = 'none';
1536
1889
  s(`.btn-restore-${idModal}`).style.display = null;
1537
1890
  s(`.btn-minimize-${idModal}`).style.display = null;
1538
- s(`.${idModal}`).style.transform = null;
1891
+ modal.style.transform = null;
1539
1892
 
1540
1893
  if (options.slideMenu) {
1541
1894
  const idSlide = this.Data[options.slideMenu]['slide-menu']
@@ -1770,6 +2123,114 @@ const Modal = {
1770
2123
  };
1771
2124
  });
1772
2125
  },
2126
+ // Move modal title element into the bar's render container so it aligns with control buttons
2127
+ /**
2128
+ * Position a modal relative to an anchor element
2129
+ * @param {Object} options - Positioning options
2130
+ * @param {string} options.modalSelector - CSS selector for the modal element
2131
+ * @param {string} options.anchorSelector - CSS selector for the anchor element
2132
+ * @param {Object} [options.offset={x: 0, y: 6}] - Offset from anchor
2133
+ * @param {string} [options.align='right'] - Horizontal alignment ('left' or 'right')
2134
+ * @param {boolean} [options.autoVertical=true] - Whether to automatically determine vertical position
2135
+ * @param {boolean} [options.placeAbove] - Force position above/below anchor (overrides autoVertical)
2136
+ */
2137
+ positionRelativeToAnchor({
2138
+ modalSelector,
2139
+ anchorSelector,
2140
+ offset = { x: 0, y: 6 },
2141
+ align = 'right',
2142
+ autoVertical = true,
2143
+ placeAbove,
2144
+ }) {
2145
+ try {
2146
+ const modal = s(modalSelector);
2147
+ const anchor = s(anchorSelector);
2148
+
2149
+ if (!modal || !anchor || !anchor.getBoundingClientRect) return;
2150
+
2151
+ // First, position the modal near its final position but off-screen
2152
+ const arect = anchor.getBoundingClientRect();
2153
+ const vh = window.innerHeight;
2154
+ const vw = window.innerWidth;
2155
+ const safeMargin = 6;
2156
+
2157
+ // Determine vertical position
2158
+ let finalPlaceAbove = placeAbove;
2159
+ if (autoVertical && placeAbove === undefined) {
2160
+ const inBottomBar = anchor.closest && anchor.closest('.bottom-bar');
2161
+ const inTopBar = anchor.closest && anchor.closest('.slide-menu-top-bar');
2162
+
2163
+ if (inBottomBar) finalPlaceAbove = true;
2164
+ else if (inTopBar) finalPlaceAbove = false;
2165
+ else finalPlaceAbove = arect.top > vh / 2; // heuristic fallback
2166
+ }
2167
+
2168
+ // Set initial position (slightly offset from final position)
2169
+ const initialOffset = finalPlaceAbove ? 20 : -20;
2170
+ modal.style.position = 'fixed';
2171
+ modal.style.opacity = '0';
2172
+ modal.style.transition = 'opacity 150ms ease-out, transform 150ms ease-out';
2173
+
2174
+ // Position near the anchor but slightly offset
2175
+ modal.style.top = `${finalPlaceAbove ? arect.top - 40 : arect.bottom + 20}px`;
2176
+ modal.style.left = `${align === 'right' ? arect.right - 200 : arect.left}px`;
2177
+ modal.style.transform = 'translateY(0)';
2178
+
2179
+ // Force reflow to ensure initial styles are applied
2180
+ modal.offsetHeight;
2181
+
2182
+ // Now calculate final position
2183
+ const mrect = modal.getBoundingClientRect();
2184
+
2185
+ // Calculate final top position
2186
+ const top = finalPlaceAbove ? arect.top - mrect.height - offset.y : arect.bottom + offset.y;
2187
+
2188
+ // Calculate final left position based on alignment
2189
+ let left;
2190
+ if (align === 'right') {
2191
+ left = arect.right - mrect.width - offset.x; // align right edges
2192
+ } else {
2193
+ left = arect.left + offset.x; // align left edges
2194
+ }
2195
+
2196
+ // Ensure modal stays within viewport bounds
2197
+ left = Math.max(safeMargin, Math.min(left, vw - mrect.width - safeMargin));
2198
+ const finalTop = Math.max(safeMargin, Math.min(top, vh - mrect.height - safeMargin));
2199
+
2200
+ // Apply final position with smooth transition
2201
+ requestAnimationFrame(() => {
2202
+ modal.style.top = `${Math.round(finalTop)}px`;
2203
+ modal.style.left = `${Math.round(left)}px`;
2204
+ modal.style.opacity = '1';
2205
+ });
2206
+ } catch (e) {
2207
+ console.error('Error positioning modal:', e);
2208
+ }
2209
+ },
2210
+
2211
+ MoveTitleToBar(idModal) {
2212
+ try {
2213
+ const titleEl = s(`.title-modal-${idModal}`);
2214
+ const container = s(`.btn-bar-modal-container-render-${idModal}`);
2215
+ if (!titleEl || !container) return;
2216
+ const titleNode = titleEl.cloneNode(true);
2217
+ titleEl.remove();
2218
+ container.classList.add('in');
2219
+ container.classList.add('fll');
2220
+ container.appendChild(titleNode);
2221
+ } catch (e) {
2222
+ // non-fatal: keep default placement if structure not present
2223
+ }
2224
+ },
2225
+ setTopBannerLink: function () {
2226
+ if (s(`.a-link-top-banner`)) {
2227
+ s(`.a-link-top-banner`).setAttribute('href', `${location.origin}${getProxyPath()}`);
2228
+ EventsUI.onClick(`.a-link-top-banner`, (e) => {
2229
+ e.preventDefault();
2230
+ s(`.action-btn-home`).click();
2231
+ });
2232
+ }
2233
+ },
1773
2234
  headerTitleHeight: 40,
1774
2235
  actionBtnCenter: function () {
1775
2236
  if (!s(`.btn-close-modal-menu`).classList.contains('hide')) {