underpost 2.8.84 → 2.8.86

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 (128) 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} +2 -2
  8. package/.github/workflows/{pwa-microservices-template.test.yml → pwa-microservices-template-test.ci.yml} +1 -1
  9. package/.github/workflows/release.cd.yml +37 -0
  10. package/.vscode/settings.json +0 -1
  11. package/README.md +16 -10
  12. package/bin/build.js +15 -5
  13. package/bin/cyberia0.js +78 -0
  14. package/bin/db.js +1 -3
  15. package/bin/deploy.js +29 -431
  16. package/bin/file.js +26 -9
  17. package/cli.md +102 -61
  18. package/conf.js +1 -1
  19. package/manifests/deployment/{dd-template-development → dd-default-development}/deployment.yaml +16 -16
  20. package/manifests/deployment/{dd-template-development → dd-default-development}/proxy.yaml +3 -3
  21. package/manifests/grafana/deployment.yaml +57 -0
  22. package/manifests/grafana/kustomization.yaml +7 -0
  23. package/manifests/grafana/pvc.yaml +12 -0
  24. package/manifests/grafana/service.yaml +14 -0
  25. package/manifests/maas/gpu-diag.sh +1 -1
  26. package/manifests/maas/ssh-cluster-info.sh +14 -0
  27. package/manifests/prometheus/deployment.yaml +82 -0
  28. package/package.json +4 -7
  29. package/src/api/user/user.router.js +24 -1
  30. package/src/api/user/user.service.js +9 -38
  31. package/src/cli/cluster.js +83 -29
  32. package/src/cli/cron.js +12 -45
  33. package/src/cli/db.js +149 -0
  34. package/src/cli/deploy.js +40 -81
  35. package/src/cli/index.js +29 -6
  36. package/src/cli/monitor.js +9 -16
  37. package/src/cli/repository.js +12 -5
  38. package/src/cli/run.js +175 -7
  39. package/src/cli/ssh.js +32 -0
  40. package/src/client/Default.index.js +7 -5
  41. package/src/client/components/core/Account.js +7 -3
  42. package/src/client/components/core/Chat.js +1 -1
  43. package/src/client/components/core/CommonJs.js +24 -22
  44. package/src/client/components/core/Content.js +12 -12
  45. package/src/client/components/core/Css.js +262 -18
  46. package/src/client/components/core/CssCore.js +8 -8
  47. package/src/client/components/core/Docs.js +14 -61
  48. package/src/client/components/core/DropDown.js +137 -82
  49. package/src/client/components/core/EventsUI.js +92 -5
  50. package/src/client/components/core/Input.js +6 -1
  51. package/src/client/components/core/LoadingAnimation.js +8 -15
  52. package/src/client/components/core/LogIn.js +3 -0
  53. package/src/client/components/core/LogOut.js +1 -1
  54. package/src/client/components/core/Modal.js +601 -137
  55. package/src/client/components/core/NotificationManager.js +2 -2
  56. package/src/client/components/core/ObjectLayerEngine.js +638 -0
  57. package/src/client/components/core/Panel.js +158 -34
  58. package/src/client/components/core/PanelForm.js +12 -3
  59. package/src/client/components/core/Recover.js +6 -3
  60. package/src/client/components/core/Router.js +77 -17
  61. package/src/client/components/core/Scroll.js +65 -120
  62. package/src/client/components/core/SignUp.js +1 -0
  63. package/src/client/components/core/SocketIo.js +3 -3
  64. package/src/client/components/core/Translate.js +6 -2
  65. package/src/client/components/core/VanillaJs.js +48 -5
  66. package/src/client/components/core/Worker.js +3 -1
  67. package/src/client/components/default/CssDefault.js +17 -3
  68. package/src/client/components/default/MenuDefault.js +266 -47
  69. package/src/client/components/default/RoutesDefault.js +8 -14
  70. package/src/client/public/default/android-chrome-144x144.png +0 -0
  71. package/src/client/public/default/android-chrome-192x192.png +0 -0
  72. package/src/client/public/default/android-chrome-256x256.png +0 -0
  73. package/src/client/public/default/android-chrome-36x36.png +0 -0
  74. package/src/client/public/default/android-chrome-48x48.png +0 -0
  75. package/src/client/public/default/android-chrome-72x72.png +0 -0
  76. package/src/client/public/default/android-chrome-96x96.png +0 -0
  77. package/src/client/public/default/apple-touch-icon-114x114-precomposed.png +0 -0
  78. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  79. package/src/client/public/default/apple-touch-icon-120x120-precomposed.png +0 -0
  80. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  81. package/src/client/public/default/apple-touch-icon-144x144-precomposed.png +0 -0
  82. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  83. package/src/client/public/default/apple-touch-icon-152x152-precomposed.png +0 -0
  84. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  85. package/src/client/public/default/apple-touch-icon-180x180-precomposed.png +0 -0
  86. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  87. package/src/client/public/default/apple-touch-icon-57x57-precomposed.png +0 -0
  88. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  89. package/src/client/public/default/apple-touch-icon-60x60-precomposed.png +0 -0
  90. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  91. package/src/client/public/default/apple-touch-icon-72x72-precomposed.png +0 -0
  92. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  93. package/src/client/public/default/apple-touch-icon-76x76-precomposed.png +0 -0
  94. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  95. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  96. package/src/client/public/default/apple-touch-icon.png +0 -0
  97. package/src/client/public/default/assets/background/dark.jpg +0 -0
  98. package/src/client/public/default/assets/background/dark.svg +557 -0
  99. package/src/client/public/default/assets/logo/base-icon.png +0 -0
  100. package/src/client/public/default/assets/logo/underpost.gif +0 -0
  101. package/src/client/public/default/assets/mailer/api-user-check.png +0 -0
  102. package/src/client/public/default/assets/mailer/api-user-invalid-token.png +0 -0
  103. package/src/client/public/default/assets/mailer/api-user-recover.png +0 -0
  104. package/src/client/public/default/favicon-16x16.png +0 -0
  105. package/src/client/public/default/favicon-32x32.png +0 -0
  106. package/src/client/public/default/favicon.ico +0 -0
  107. package/src/client/public/default/mstile-144x144.png +0 -0
  108. package/src/client/public/default/mstile-150x150.png +0 -0
  109. package/src/client/public/default/mstile-310x150.png +0 -0
  110. package/src/client/public/default/mstile-310x310.png +0 -0
  111. package/src/client/public/default/mstile-70x70.png +0 -0
  112. package/src/client/public/default/safari-pinned-tab.svg +24 -0
  113. package/src/client/ssr/body/DefaultSplashScreen.js +2 -2
  114. package/src/index.js +9 -1
  115. package/src/mailer/MailerProvider.js +37 -0
  116. package/src/monitor.js +24 -0
  117. package/src/runtime/lampp/Dockerfile +30 -39
  118. package/src/runtime/lampp/Lampp.js +11 -2
  119. package/src/server/client-build-docs.js +205 -0
  120. package/src/server/client-build-live.js +1 -1
  121. package/src/server/client-build.js +16 -166
  122. package/src/server/client-dev-server.js +1 -1
  123. package/src/server/conf.js +14 -277
  124. package/src/server/proxy.js +1 -2
  125. package/src/server/start.js +3 -3
  126. package/src/server/valkey.js +102 -41
  127. package/docker-compose.yml +0 -67
  128. package/prometheus.yml +0 -36
@@ -24,11 +24,11 @@ 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';
31
- import { Input } from './Input.js';
31
+ import { Input, isTextInputFocused } from './Input.js';
32
32
  import { Validator } from './Validator.js';
33
33
  import { DropDown } from './DropDown.js';
34
34
  import { Keyboard } from './Keyboard.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.BannerAppTemplate}
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 fl">
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 = [];
@@ -507,6 +608,7 @@ const Modal = {
507
608
  }),
508
609
  );
509
610
  s(`.search-result-btn-${result.routerId}`).onclick = () => {
611
+ if (!s(`.html-${searchBoxHistoryId}`) || !s(`.html-${searchBoxHistoryId}`).hasChildNodes()) return;
510
612
  s(`.html-${searchBoxHistoryId}`).childNodes[currentKeyBoardSearchBoxIndex].classList.remove(
511
613
  `main-btn-menu-active`,
512
614
  );
@@ -520,6 +622,7 @@ const Modal = {
520
622
  };
521
623
 
522
624
  const getResultSearchBox = (validatorData) => {
625
+ if (!s(`.html-${searchBoxHistoryId}`) || !s(`.html-${searchBoxHistoryId}`).hasChildNodes()) return;
523
626
  const { model, id } = validatorData;
524
627
  switch (model) {
525
628
  case 'search-box':
@@ -592,17 +695,29 @@ const Modal = {
592
695
  const isSearchBoxActiveElement = isActiveElement(inputSearchBoxId);
593
696
  checkHistoryBoxTitleStatus();
594
697
  checkShortcutContainerInfoEnabled();
595
- if (!isSearchBoxActiveElement && !hoverHistBox && !hoverInputBox) {
698
+ if (!isSearchBoxActiveElement && !hoverFocusCtl.shouldStay()) {
596
699
  Modal.removeModal(searchBoxHistoryId);
597
700
  return;
598
701
  }
599
- setTimeout(() => getResultSearchBox(validatorData));
702
+ setTimeout(() => {
703
+ getResultSearchBox(validatorData);
704
+
705
+ if (
706
+ s(`.slide-menu-top-bar-fix`) &&
707
+ !s(`.main-body-btn-ui-bar-custom-open`).classList.contains('hide')
708
+ ) {
709
+ s(`.main-body-btn-bar-custom`).click();
710
+ }
711
+ });
600
712
  };
601
713
 
602
714
  const getDefaultSearchBoxSelector = () => `.search-result-btn-${currentKeyBoardSearchBoxIndex}`;
603
715
 
604
716
  const updateSearchBoxValue = (selector) => {
605
717
  if (!selector) selector = getDefaultSearchBoxSelector();
718
+ // check exist childNodes
719
+ if (!s(selector) || !s(selector).hasChildNodes()) return;
720
+
606
721
  if (s(selector).childNodes) {
607
722
  if (
608
723
  s(selector).childNodes[s(selector).childNodes.length - 1] &&
@@ -626,6 +741,10 @@ const Modal = {
626
741
 
627
742
  const setSearchValue = (selector) => {
628
743
  if (!selector) selector = getDefaultSearchBoxSelector();
744
+
745
+ // check exist childNodes
746
+ if (!s(selector) || !s(selector).hasChildNodes()) return;
747
+
629
748
  historySearchBox = historySearchBox.filter(
630
749
  (h) => h.routerId !== results[currentKeyBoardSearchBoxIndex].routerId,
631
750
  );
@@ -676,27 +795,19 @@ const Modal = {
676
795
  barMode: options.barMode,
677
796
  });
678
797
 
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);
798
+ // Bind hover/focus and click-outside to dismiss
799
+ hoverFocusCtl.bind();
800
+ unbindDocSearch = EventsUI.bindDismissOnDocumentClick({
801
+ shouldStay: hoverFocusCtl.shouldStay,
802
+ onDismiss: () => dismissSearchBox(),
803
+ anchors: [`.top-bar-search-box-container`, `.${id}`],
804
+ });
805
+ // Ensure cleanup when modal closes
806
+ Modal.Data[id].onCloseListener[`unbind-doc-${id}`] = () => unbindDocSearch && unbindDocSearch();
684
807
 
685
- prepend(`.btn-bar-modal-container-${id}`, html`<div class="hide">${inputInfoNode.outerHTML}</div>`);
808
+ Modal.MoveTitleToBar(id);
686
809
 
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
- };
810
+ prepend(`.btn-bar-modal-container-${id}`, html`<div class="hide">${inputInfoNode.outerHTML}</div>`);
700
811
  }
701
812
  };
702
813
 
@@ -708,14 +819,23 @@ const Modal = {
708
819
  searchBoxHistoryOpen();
709
820
  searchBoxCallBack(formDataInfoNode[0]);
710
821
  };
711
- s('.top-bar-search-box').onblur = () => {
712
- if (!hoverHistBox && !hoverInputBox && !isActiveElement(inputSearchBoxId)) {
713
- Modal.removeModal(searchBoxHistoryId);
822
+
823
+ const dismissSearchBox = () => {
824
+ if (unbindDocSearch) {
825
+ try {
826
+ unbindDocSearch();
827
+ } catch (e) {}
714
828
  }
829
+ Modal.removeModal(searchBoxHistoryId);
830
+ };
831
+ s('.top-bar-search-box').onblur = () => {
832
+ hoverFocusCtl.checkDismiss();
715
833
  };
716
834
  EventsUI.onClick(`.top-bar-search-box-container`, () => {
717
835
  searchBoxHistoryOpen();
718
836
  searchBoxCallBack(formDataInfoNode[0]);
837
+ const inputEl = s(`.${inputSearchBoxId}`);
838
+ if (inputEl && inputEl.focus) inputEl.focus();
719
839
  });
720
840
 
721
841
  const timePressDelay = 100;
@@ -880,7 +1000,11 @@ const Modal = {
880
1000
  ['Alt', 'k'],
881
1001
  ],
882
1002
  eventCallBack: () => {
1003
+ if (isTextInputFocused()) return;
883
1004
  if (s(`.top-bar-search-box`)) {
1005
+ if (s(`.main-body-btn-ui-close`).classList.contains('hide')) {
1006
+ s(`.main-body-btn-ui-open`).click();
1007
+ }
884
1008
  s(`.top-bar-search-box`).blur();
885
1009
  s(`.top-bar-search-box`).focus();
886
1010
  s(`.top-bar-search-box`).select();
@@ -897,8 +1021,10 @@ const Modal = {
897
1021
  barConfig.buttons.menu.disabled = true;
898
1022
  barConfig.buttons.close.disabled = true;
899
1023
  const id = 'bottom-bar';
900
- if (options && options.homeModals && !options.homeModals.includes(id)) options.homeModals.push(id);
901
- else options.homeModals = [id];
1024
+ if (!this.Data[idModal].homeModals) this.Data[idModal].homeModals = [];
1025
+ if (!this.Data[idModal].homeModals.includes(id)) {
1026
+ this.Data[idModal].homeModals.push(id);
1027
+ }
902
1028
  const html = async () => html`
903
1029
  <style>
904
1030
  .top-bar-search-box-container {
@@ -1015,7 +1141,7 @@ const Modal = {
1015
1141
  await Modal.onHomeRouterEvent();
1016
1142
  Object.keys(this.Data[idModal].onHome).map((keyListener) => this.Data[idModal].onHome[keyListener]());
1017
1143
  });
1018
- EventsUI.onClick(`.action-btn-app-icon`, () => Modal.onHomeRouterEvent());
1144
+ EventsUI.onClick(`.action-btn-app-icon`, () => s(`.action-btn-home`).click());
1019
1145
  Keyboard.instanceMultiPressKey({
1020
1146
  id: 'input-shortcut-global-escape',
1021
1147
  keys: ['Escape'],
@@ -1026,11 +1152,18 @@ const Modal = {
1026
1152
  }
1027
1153
 
1028
1154
  {
1029
- ThemeEvents['action-btn-theme'] = () => {
1155
+ ThemeEvents['action-btn-theme'] = async () => {
1030
1156
  htmls(
1031
1157
  `.action-btn-theme-render`,
1032
1158
  html` ${darkTheme ? html` <i class="fas fa-moon"></i>` : html`<i class="far fa-sun"></i>`}`,
1033
1159
  );
1160
+ if (s(`.slide-menu-top-bar-fix`)) {
1161
+ htmls(
1162
+ `.slide-menu-top-bar-fix`,
1163
+ html`<a class="a-link-top-banner fl">${await options.slideMenuTopBarBannerFix()}</a>`,
1164
+ );
1165
+ Modal.setTopBannerLink();
1166
+ }
1034
1167
  };
1035
1168
  ThemeEvents['action-btn-theme']();
1036
1169
 
@@ -1048,13 +1181,80 @@ const Modal = {
1048
1181
 
1049
1182
  {
1050
1183
  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
- });
1184
+ // old method
1185
+ // EventsUI.onClick(`.action-btn-lang`, () => {
1186
+ // let lang = 'en';
1187
+ // if (s('html').lang === 'en') lang = 'es';
1188
+ // if (s(`.dropdown-option-${lang}`))
1189
+ // DropDown.Tokens['settings-lang'].onClickEvents[`dropdown-option-${lang}`]();
1190
+ // else Translate.renderLang(lang);
1191
+ // });
1192
+
1193
+ // Open lightweight empty modal on language button, with shared dismiss logic
1194
+ EventsUI.onClick(
1195
+ `.action-btn-lang`,
1196
+ async () => {
1197
+ const id = 'action-btn-lang-modal';
1198
+ if (s(`.${id}`)) {
1199
+ return s(`.btn-close-${id}`).click();
1200
+ }
1201
+ const { barConfig } = await Themes[Css.currentTheme]();
1202
+ barConfig.buttons.maximize.disabled = true;
1203
+ barConfig.buttons.minimize.disabled = true;
1204
+ barConfig.buttons.restore.disabled = true;
1205
+ barConfig.buttons.menu.disabled = true;
1206
+ barConfig.buttons.close.disabled = false;
1207
+ await Modal.Render({
1208
+ id,
1209
+ barConfig,
1210
+ title: html`${renderViewTitle({
1211
+ icon: html`<i class="fas fa-language mini-title"></i>`,
1212
+ text: Translate.Render('select lang'),
1213
+ })}`,
1214
+ html: async () => html`${await Translate.RenderSetting('action-drop-modal' + id)}`,
1215
+ titleClass: 'mini-title',
1216
+ style: {
1217
+ resize: 'none',
1218
+ width: '100% !important',
1219
+ height: '350px !important',
1220
+ 'max-width': '350px !important',
1221
+ 'z-index': 7,
1222
+ },
1223
+ dragDisabled: true,
1224
+ maximize: true,
1225
+ heightBottomBar: 0,
1226
+ heightTopBar: originHeightTopBar,
1227
+ barMode: options.barMode,
1228
+ });
1229
+
1230
+ // Move title inside the bar container to align with control buttons
1231
+ Modal.MoveTitleToBar(id);
1232
+
1233
+ // Position the language selection modal relative to the language button
1234
+ Modal.positionRelativeToAnchor({
1235
+ modalSelector: `.${id}`,
1236
+ anchorSelector: '.action-btn-lang',
1237
+ align: 'right',
1238
+ offset: { x: 0, y: 6 },
1239
+ autoVertical: true,
1240
+ });
1241
+
1242
+ // Hover/focus controller uses the button as input anchor
1243
+ const hoverFocusCtl = EventsUI.HoverFocusController({
1244
+ inputSelector: `.action-btn-lang`,
1245
+ panelSelector: `.${id}`,
1246
+ onDismiss: () => Modal.removeModal(id),
1247
+ });
1248
+ hoverFocusCtl.bind();
1249
+ const unbindDoc = EventsUI.bindDismissOnDocumentClick({
1250
+ shouldStay: hoverFocusCtl.shouldStay,
1251
+ onDismiss: () => Modal.removeModal(id),
1252
+ anchors: [`.action-btn-lang`, `.${id}`],
1253
+ });
1254
+ Modal.Data[id].onCloseListener[`unbind-doc-${id}`] = () => unbindDoc();
1255
+ },
1256
+ { context: 'modal', noGate: true, noLoading: true },
1257
+ );
1058
1258
  }
1059
1259
 
1060
1260
  {
@@ -1348,19 +1548,18 @@ const Modal = {
1348
1548
  this.onHomeRouterEvent = async () => {
1349
1549
  for (const keyModal of Object.keys(this.Data)) {
1350
1550
  if (
1351
- ![idModal, 'main-body-top', 'main-body']
1352
- .concat(options?.homeModals ? options.homeModals : [])
1353
- .includes(keyModal)
1551
+ ![idModal, 'main-body-top', 'main-body'].concat(this.Data[idModal]?.homeModals || []).includes(keyModal)
1354
1552
  )
1355
- s(`.btn-close-${keyModal}`).click();
1553
+ if (s(`.btn-close-${keyModal}`)) s(`.btn-close-${keyModal}`).click();
1356
1554
  backMenuButtonEvent();
1357
1555
  }
1358
- s(`.btn-close-modal-menu`).click();
1556
+ if (s(`.btn-close-modal-menu`)) s(`.btn-close-modal-menu`).click();
1359
1557
  setPath(getProxyPath());
1360
- setDocTitle({ ...options.RouterInstance, route: '' });
1558
+ setDocTitle();
1361
1559
  };
1362
1560
  s(`.main-btn-home`).onclick = async () => {
1363
- await this.onHomeRouterEvent();
1561
+ // await this.onHomeRouterEvent();
1562
+ s(`.action-btn-home`).click();
1364
1563
  };
1365
1564
  EventsUI.onClick(`.btn-icon-menu-back`, backMenuButtonEvent);
1366
1565
  EventsUI.onClick(`.btn-icon-menu-mode`, () => {
@@ -1429,42 +1628,139 @@ const Modal = {
1429
1628
  default:
1430
1629
  break;
1431
1630
  }
1631
+ // Track drag position for consistency
1632
+ let dragPosition = { x: 0, y: 0 };
1633
+
1634
+ // Initialize drag options with proper bounds and smooth transitions
1432
1635
  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;
1636
+ handle: handle,
1637
+ bounds: {
1638
+ top: 0,
1639
+ left: 0,
1640
+ right: 0,
1641
+ bottom: 0,
1642
+ },
1643
+ preventDefault: true,
1644
+ position: { x: 0, y: 0 },
1645
+ onDragStart: () => {
1646
+ const modal = s(`.${idModal}`);
1647
+ if (!modal) return false; // Prevent drag if modal not found
1648
+
1649
+ // Store current position
1650
+ const computedStyle = window.getComputedStyle(modal);
1651
+ const matrix = new DOMMatrixReadOnly(computedStyle.transform);
1652
+ dragPosition = {
1653
+ x: matrix.m41 || 0,
1654
+ y: matrix.m42 || 0,
1655
+ };
1656
+
1657
+ modal.style.transition = 'none';
1658
+ modal.style.willChange = 'transform';
1659
+ return true; // Allow drag to start
1439
1660
  },
1440
1661
  onDrag: (data) => {
1441
- if (!s(`.${idModal}`)) return;
1442
- // logger.info('Dragging', data);
1662
+ // Update position based on drag delta
1663
+ dragPosition = { x: data.x, y: data.y };
1443
1664
  },
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
- );
1665
+ onDragEnd: () => {
1666
+ const modal = s(`.${idModal}`);
1667
+ if (!modal) return;
1668
+
1669
+ modal.style.willChange = '';
1670
+ modal.style.transition = transition;
1671
+
1672
+ // Update drag instance with current position
1673
+ if (dragInstance) {
1674
+ dragInstance.updateOptions({
1675
+ position: dragPosition,
1676
+ });
1677
+ }
1678
+
1679
+ // Notify listeners
1680
+ Object.keys(this.Data[idModal].onDragEndListener || {}).forEach((keyListener) => {
1681
+ this.Data[idModal].onDragEndListener[keyListener]?.();
1682
+ });
1451
1683
  },
1452
1684
  };
1453
- let dragInstance;
1454
- // new Draggable(s(`.${idModal}`), { disabled: true });
1455
- const setDragInstance = () => (options?.dragDisabled ? null : new Draggable(s(`.${idModal}`), dragOptions));
1685
+
1686
+ let dragInstance = null;
1687
+
1688
+ // Initialize or update drag instance
1689
+ const setDragInstance = () => {
1690
+ if (options?.dragDisabled) {
1691
+ if (dragInstance) {
1692
+ dragInstance.destroy();
1693
+ dragInstance = null;
1694
+ }
1695
+ return null;
1696
+ }
1697
+
1698
+ const modal = s(`.${idModal}`);
1699
+ if (!modal) {
1700
+ console.warn(`Modal element .${idModal} not found for drag initialization`);
1701
+ return null;
1702
+ }
1703
+
1704
+ // Ensure the modal has position: absolute for proper dragging
1705
+ if (window.getComputedStyle(modal).position !== 'absolute') {
1706
+ modal.style.position = 'absolute';
1707
+ }
1708
+
1709
+ // Clean up existing instance
1710
+ if (dragInstance) {
1711
+ dragInstance.destroy();
1712
+ }
1713
+
1714
+ try {
1715
+ // Create new instance with updated options
1716
+ dragInstance = new Draggable(modal, dragOptions);
1717
+ return dragInstance;
1718
+ } catch (error) {
1719
+ console.error('Failed to initialize draggable:', error);
1720
+ return null;
1721
+ }
1722
+ };
1723
+
1724
+ // Expose method to update drag options
1456
1725
  this.Data[idModal].setDragInstance = (updateDragOptions) => {
1457
- dragOptions = {
1458
- ...dragOptions,
1459
- ...updateDragOptions,
1460
- };
1726
+ if (updateDragOptions) {
1727
+ dragOptions = { ...dragOptions, ...updateDragOptions };
1728
+ }
1461
1729
  dragInstance = setDragInstance();
1462
1730
  this.Data[idModal].dragInstance = dragInstance;
1463
1731
  this.Data[idModal].dragOptions = dragOptions;
1464
1732
  };
1465
- s(`.${idModal}`).style.transition = '0.15s';
1466
- setTimeout(() => (s(`.${idModal}`).style.opacity = '1'));
1467
- setTimeout(() => (s(`.${idModal}`).style.transition = transition), 150);
1733
+ // Initialize modal with proper transitions
1734
+ const modal = s(`.${idModal}`);
1735
+ if (modal) {
1736
+ // Initial state
1737
+ modal.style.transition = 'opacity 0.15s ease, transform 0.3s ease';
1738
+ modal.style.opacity = '0';
1739
+
1740
+ // Trigger fade-in after a small delay to allow initial render
1741
+ requestAnimationFrame(() => {
1742
+ if (!modal) return;
1743
+ modal.style.opacity = '1';
1744
+
1745
+ // Set final transition after initial animation completes
1746
+ setTimeout(() => {
1747
+ if (modal) {
1748
+ modal.style.transition = transition;
1749
+
1750
+ // Initialize drag after transitions are set
1751
+ if (!options.dragDisabled) {
1752
+ setDragInstance();
1753
+ if (!options.mode) {
1754
+ dragInstance.updateOptions({
1755
+ position: { x: 0, y: 0 },
1756
+ disabled: false, // Ensure drag is enabled after restore
1757
+ });
1758
+ }
1759
+ }
1760
+ }
1761
+ }, 150);
1762
+ });
1763
+ }
1468
1764
 
1469
1765
  const btnCloseEvent = () => {
1470
1766
  Object.keys(this.Data[idModal].onCloseListener).map((keyListener) =>
@@ -1480,62 +1776,122 @@ const Modal = {
1480
1776
  setTimeout(() => {
1481
1777
  if (!s(`.${idModal}`)) return;
1482
1778
  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
- })();
1779
+ // Handle modal route change
1780
+ if (options.route) {
1781
+ closeModalRouteChangeEvent({
1782
+ route: options.route,
1783
+ RouterInstance: options.RouterInstance,
1784
+ homeCid: Modal.homeCid,
1785
+ });
1786
+ }
1504
1787
  }, 300);
1505
1788
  };
1506
1789
  s(`.btn-close-${idModal}`).onclick = btnCloseEvent;
1507
1790
 
1791
+ // Minimize button handler
1508
1792
  s(`.btn-minimize-${idModal}`).onclick = () => {
1509
- if (options.slideMenu) delete this.Data[idModal].slideMenu;
1510
- s(`.${idModal}`).style.transition = '0.3s';
1793
+ const modal = s(`.${idModal}`);
1794
+ if (!modal) return;
1795
+
1796
+ if (options.slideMenu) {
1797
+ delete this.Data[idModal].slideMenu;
1798
+ }
1799
+
1800
+ // Keep drag enabled when minimized
1801
+ if (dragInstance) {
1802
+ dragInstance.updateOptions({ disabled: false });
1803
+ }
1804
+
1805
+ // Set up transition
1806
+ modal.style.transition = 'height 0.3s ease, transform 0.3s ease';
1807
+
1808
+ // Update button states
1511
1809
  s(`.btn-minimize-${idModal}`).style.display = 'none';
1512
1810
  s(`.btn-maximize-${idModal}`).style.display = null;
1513
1811
  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);
1812
+
1813
+ // Collapse to header height
1814
+ const header = s(`.bar-default-modal-${idModal}`);
1815
+ if (header) {
1816
+ modal.style.height = `${header.clientHeight}px`;
1817
+ modal.style.overflow = 'hidden';
1818
+ }
1819
+
1820
+ // Restore transition after animation
1821
+ setTimeout(() => {
1822
+ if (modal) {
1823
+ modal.style.transition = transition;
1824
+ }
1825
+ }, 300);
1516
1826
  };
1827
+ // Restore button handler
1517
1828
  s(`.btn-restore-${idModal}`).onclick = () => {
1518
- if (options.slideMenu) delete this.Data[idModal].slideMenu;
1519
- s(`.${idModal}`).style.transition = '0.3s';
1829
+ const modal = s(`.${idModal}`);
1830
+ if (!modal) return;
1831
+
1832
+ if (options.slideMenu) {
1833
+ delete this.Data[idModal].slideMenu;
1834
+ }
1835
+
1836
+ // Re-enable dragging
1837
+ if (dragInstance) {
1838
+ dragInstance.updateOptions({ disabled: false });
1839
+ }
1840
+
1841
+ // Set up transition
1842
+ modal.style.transition = 'all 0.3s ease';
1843
+
1844
+ // Update button states
1520
1845
  s(`.btn-restore-${idModal}`).style.display = 'none';
1521
1846
  s(`.btn-minimize-${idModal}`).style.display = null;
1522
1847
  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);
1848
+
1849
+ // Restore original dimensions and position
1850
+ modal.style.transform = '';
1851
+ modal.style.height = '';
1852
+ left = 0;
1853
+ width = 300;
1854
+ modal.style.left = `${left}px`;
1855
+ modal.style.width = `${width}px`;
1856
+ modal.style.overflow = '';
1857
+
1858
+ // Reset drag position
1859
+ dragPosition = { x: 0, y: 0 };
1860
+
1861
+ // Set new position
1862
+ modal.style.transform = `translate(0, 0)`;
1863
+
1864
+ // Adjust top position based on top bar visibility
1865
+ const heightDefaultTopBar = 40; // Default top bar height if not specified
1866
+ s(`.${idModal}`).style.top = s(`.main-body-btn-ui-close`).classList.contains('hide')
1867
+ ? `0px`
1868
+ : `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`;
1869
+
1870
+ // Re-enable drag after restore
1871
+ if (dragInstance) {
1872
+ dragInstance.updateOptions({
1873
+ position: { x: 0, y: 0 },
1874
+ disabled: false, // Ensure drag is enabled after restore
1875
+ });
1876
+ }
1877
+ setTimeout(() => (s(`.${idModal}`) ? (s(`.${idModal}`).style.transition = transition) : null), 300);
1531
1878
  };
1532
1879
  s(`.btn-maximize-${idModal}`).onclick = () => {
1533
- s(`.${idModal}`).style.transition = '0.3s';
1534
- setTimeout(() => (s(`.${idModal}`).style.transition = transition), 300);
1880
+ const modal = s(`.${idModal}`);
1881
+ if (!modal) return;
1882
+
1883
+ // Disable drag when maximizing
1884
+ if (dragInstance) {
1885
+ dragInstance.updateOptions({ disabled: true });
1886
+ }
1887
+
1888
+ modal.style.transition = '0.3s';
1889
+ setTimeout(() => (modal ? (modal.style.transition = transition) : null), 300);
1890
+
1535
1891
  s(`.btn-maximize-${idModal}`).style.display = 'none';
1536
1892
  s(`.btn-restore-${idModal}`).style.display = null;
1537
1893
  s(`.btn-minimize-${idModal}`).style.display = null;
1538
- s(`.${idModal}`).style.transform = null;
1894
+ modal.style.transform = null;
1539
1895
 
1540
1896
  if (options.slideMenu) {
1541
1897
  const idSlide = this.Data[options.slideMenu]['slide-menu']
@@ -1770,6 +2126,114 @@ const Modal = {
1770
2126
  };
1771
2127
  });
1772
2128
  },
2129
+ // Move modal title element into the bar's render container so it aligns with control buttons
2130
+ /**
2131
+ * Position a modal relative to an anchor element
2132
+ * @param {Object} options - Positioning options
2133
+ * @param {string} options.modalSelector - CSS selector for the modal element
2134
+ * @param {string} options.anchorSelector - CSS selector for the anchor element
2135
+ * @param {Object} [options.offset={x: 0, y: 6}] - Offset from anchor
2136
+ * @param {string} [options.align='right'] - Horizontal alignment ('left' or 'right')
2137
+ * @param {boolean} [options.autoVertical=true] - Whether to automatically determine vertical position
2138
+ * @param {boolean} [options.placeAbove] - Force position above/below anchor (overrides autoVertical)
2139
+ */
2140
+ positionRelativeToAnchor({
2141
+ modalSelector,
2142
+ anchorSelector,
2143
+ offset = { x: 0, y: 6 },
2144
+ align = 'right',
2145
+ autoVertical = true,
2146
+ placeAbove,
2147
+ }) {
2148
+ try {
2149
+ const modal = s(modalSelector);
2150
+ const anchor = s(anchorSelector);
2151
+
2152
+ if (!modal || !anchor || !anchor.getBoundingClientRect) return;
2153
+
2154
+ // First, position the modal near its final position but off-screen
2155
+ const arect = anchor.getBoundingClientRect();
2156
+ const vh = window.innerHeight;
2157
+ const vw = window.innerWidth;
2158
+ const safeMargin = 6;
2159
+
2160
+ // Determine vertical position
2161
+ let finalPlaceAbove = placeAbove;
2162
+ if (autoVertical && placeAbove === undefined) {
2163
+ const inBottomBar = anchor.closest && anchor.closest('.bottom-bar');
2164
+ const inTopBar = anchor.closest && anchor.closest('.slide-menu-top-bar');
2165
+
2166
+ if (inBottomBar) finalPlaceAbove = true;
2167
+ else if (inTopBar) finalPlaceAbove = false;
2168
+ else finalPlaceAbove = arect.top > vh / 2; // heuristic fallback
2169
+ }
2170
+
2171
+ // Set initial position (slightly offset from final position)
2172
+ const initialOffset = finalPlaceAbove ? 20 : -20;
2173
+ modal.style.position = 'fixed';
2174
+ modal.style.opacity = '0';
2175
+ modal.style.transition = 'opacity 150ms ease-out, transform 150ms ease-out';
2176
+
2177
+ // Position near the anchor but slightly offset
2178
+ modal.style.top = `${finalPlaceAbove ? arect.top - 40 : arect.bottom + 20}px`;
2179
+ modal.style.left = `${align === 'right' ? arect.right - 200 : arect.left}px`;
2180
+ modal.style.transform = 'translateY(0)';
2181
+
2182
+ // Force reflow to ensure initial styles are applied
2183
+ modal.offsetHeight;
2184
+
2185
+ // Now calculate final position
2186
+ const mrect = modal.getBoundingClientRect();
2187
+
2188
+ // Calculate final top position
2189
+ const top = finalPlaceAbove ? arect.top - mrect.height - offset.y : arect.bottom + offset.y;
2190
+
2191
+ // Calculate final left position based on alignment
2192
+ let left;
2193
+ if (align === 'right') {
2194
+ left = arect.right - mrect.width - offset.x; // align right edges
2195
+ } else {
2196
+ left = arect.left + offset.x; // align left edges
2197
+ }
2198
+
2199
+ // Ensure modal stays within viewport bounds
2200
+ left = Math.max(safeMargin, Math.min(left, vw - mrect.width - safeMargin));
2201
+ const finalTop = Math.max(safeMargin, Math.min(top, vh - mrect.height - safeMargin));
2202
+
2203
+ // Apply final position with smooth transition
2204
+ requestAnimationFrame(() => {
2205
+ modal.style.top = `${Math.round(finalTop)}px`;
2206
+ modal.style.left = `${Math.round(left)}px`;
2207
+ modal.style.opacity = '1';
2208
+ });
2209
+ } catch (e) {
2210
+ console.error('Error positioning modal:', e);
2211
+ }
2212
+ },
2213
+
2214
+ MoveTitleToBar(idModal) {
2215
+ try {
2216
+ const titleEl = s(`.title-modal-${idModal}`);
2217
+ const container = s(`.btn-bar-modal-container-render-${idModal}`);
2218
+ if (!titleEl || !container) return;
2219
+ const titleNode = titleEl.cloneNode(true);
2220
+ titleEl.remove();
2221
+ container.classList.add('in');
2222
+ container.classList.add('fll');
2223
+ container.appendChild(titleNode);
2224
+ } catch (e) {
2225
+ // non-fatal: keep default placement if structure not present
2226
+ }
2227
+ },
2228
+ setTopBannerLink: function () {
2229
+ if (s(`.a-link-top-banner`)) {
2230
+ s(`.a-link-top-banner`).setAttribute('href', `${location.origin}${getProxyPath()}`);
2231
+ EventsUI.onClick(`.a-link-top-banner`, (e) => {
2232
+ e.preventDefault();
2233
+ s(`.action-btn-home`).click();
2234
+ });
2235
+ }
2236
+ },
1773
2237
  headerTitleHeight: 40,
1774
2238
  actionBtnCenter: function () {
1775
2239
  if (!s(`.btn-close-modal-menu`).classList.contains('hide')) {