underpost 2.8.843 → 2.8.845

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 (35) hide show
  1. package/.github/workflows/{ghpkg.yml → ghpkg.ci.yml} +1 -1
  2. package/.github/workflows/{npmpkg.yml → npmpkg.ci.yml} +1 -1
  3. package/.github/workflows/{publish.yml → publish.ci.yml} +1 -1
  4. package/.github/workflows/{pwa-microservices-template.page.yml → pwa-microservices-template-page.cd.yml} +1 -1
  5. package/.github/workflows/{pwa-microservices-template.test.yml → pwa-microservices-template-test.ci.yml} +1 -1
  6. package/.vscode/settings.json +0 -1
  7. package/README.md +18 -2
  8. package/bin/build.js +8 -5
  9. package/bin/deploy.js +10 -69
  10. package/bin/file.js +15 -11
  11. package/cli.md +47 -43
  12. package/docker-compose.yml +1 -1
  13. package/manifests/deployment/dd-template-development/deployment.yaml +2 -2
  14. package/manifests/maas/gpu-diag.sh +1 -1
  15. package/package.json +3 -5
  16. package/src/api/user/user.router.js +24 -1
  17. package/src/cli/cluster.js +19 -10
  18. package/src/cli/index.js +1 -0
  19. package/src/cli/run.js +21 -0
  20. package/src/client/components/core/Css.js +52 -2
  21. package/src/client/components/core/CssCore.js +0 -4
  22. package/src/client/components/core/Docs.js +10 -57
  23. package/src/client/components/core/DropDown.js +128 -82
  24. package/src/client/components/core/EventsUI.js +92 -5
  25. package/src/client/components/core/Modal.js +451 -120
  26. package/src/client/components/core/NotificationManager.js +2 -2
  27. package/src/client/components/core/Panel.js +2 -2
  28. package/src/client/components/core/PanelForm.js +12 -2
  29. package/src/client/components/core/Recover.js +1 -1
  30. package/src/client/components/core/Router.js +63 -2
  31. package/src/client/components/core/Translate.js +2 -2
  32. package/src/index.js +1 -1
  33. package/src/server/client-build-docs.js +205 -0
  34. package/src/server/client-build.js +11 -140
  35. 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,19 +126,13 @@ 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':
@@ -434,8 +430,14 @@ const Modal = {
434
430
  rules: [] /*{ type: 'isEmpty' }, { type: 'isEmail' }*/,
435
431
  },
436
432
  ];
437
- let hoverHistBox = false;
438
- let hoverInputBox = false;
433
+ // Reusable hover/focus controller for search history panel
434
+ let unbindDocSearch = null;
435
+ const hoverFocusCtl = EventsUI.HoverFocusController({
436
+ inputSelector: `.top-bar-search-box-container`,
437
+ panelSelector: `.${id}`,
438
+ activeElementId: inputSearchBoxId,
439
+ onDismiss: () => dismissSearchBox(),
440
+ });
439
441
  let currentKeyBoardSearchBoxIndex = 0;
440
442
  let results = [];
441
443
  let historySearchBox = [];
@@ -592,7 +594,7 @@ const Modal = {
592
594
  const isSearchBoxActiveElement = isActiveElement(inputSearchBoxId);
593
595
  checkHistoryBoxTitleStatus();
594
596
  checkShortcutContainerInfoEnabled();
595
- if (!isSearchBoxActiveElement && !hoverHistBox && !hoverInputBox) {
597
+ if (!isSearchBoxActiveElement && !hoverFocusCtl.shouldStay()) {
596
598
  Modal.removeModal(searchBoxHistoryId);
597
599
  return;
598
600
  }
@@ -676,27 +678,20 @@ const Modal = {
676
678
  barMode: options.barMode,
677
679
  });
678
680
 
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);
681
+ // Bind hover/focus and click-outside to dismiss
682
+ hoverFocusCtl.bind();
683
+ unbindDocSearch = EventsUI.bindDismissOnDocumentClick({
684
+ shouldStay: hoverFocusCtl.shouldStay,
685
+ onDismiss: () => dismissSearchBox(),
686
+ anchors: [`.top-bar-search-box-container`, `.${id}`],
687
+ });
688
+ // Ensure cleanup when modal closes
689
+ Modal.Data[id].onCloseListener[`unbind-doc-${id}`] = () => unbindDocSearch && unbindDocSearch();
684
690
 
685
- prepend(`.btn-bar-modal-container-${id}`, html`<div class="hide">${inputInfoNode.outerHTML}</div>`);
691
+ Modal.MoveTitleToBar(id);
686
692
 
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
- };
693
+ prepend(`.btn-bar-modal-container-${id}`, html`<div class="hide">${inputInfoNode.outerHTML}</div>`);
694
+ if (s(`.slide-menu-top-bar-fix`)) s(`.slide-menu-top-bar-fix`).classList.add('hide');
700
695
  }
701
696
  };
702
697
 
@@ -708,14 +703,24 @@ const Modal = {
708
703
  searchBoxHistoryOpen();
709
704
  searchBoxCallBack(formDataInfoNode[0]);
710
705
  };
711
- s('.top-bar-search-box').onblur = () => {
712
- if (!hoverHistBox && !hoverInputBox && !isActiveElement(inputSearchBoxId)) {
713
- Modal.removeModal(searchBoxHistoryId);
706
+
707
+ const dismissSearchBox = () => {
708
+ if (unbindDocSearch) {
709
+ try {
710
+ unbindDocSearch();
711
+ } catch (e) {}
714
712
  }
713
+ Modal.removeModal(searchBoxHistoryId);
714
+ if (s(`.slide-menu-top-bar-fix`)) s(`.slide-menu-top-bar-fix`).classList.remove('hide');
715
+ };
716
+ s('.top-bar-search-box').onblur = () => {
717
+ hoverFocusCtl.checkDismiss();
715
718
  };
716
719
  EventsUI.onClick(`.top-bar-search-box-container`, () => {
717
720
  searchBoxHistoryOpen();
718
721
  searchBoxCallBack(formDataInfoNode[0]);
722
+ const inputEl = s(`.${inputSearchBoxId}`);
723
+ if (inputEl && inputEl.focus) inputEl.focus();
719
724
  });
720
725
 
721
726
  const timePressDelay = 100;
@@ -881,6 +886,9 @@ const Modal = {
881
886
  ],
882
887
  eventCallBack: () => {
883
888
  if (s(`.top-bar-search-box`)) {
889
+ if (s(`.main-body-btn-ui-close`).classList.contains('hide')) {
890
+ s(`.main-body-btn-ui-open`).click();
891
+ }
884
892
  s(`.top-bar-search-box`).blur();
885
893
  s(`.top-bar-search-box`).focus();
886
894
  s(`.top-bar-search-box`).select();
@@ -897,8 +905,10 @@ const Modal = {
897
905
  barConfig.buttons.menu.disabled = true;
898
906
  barConfig.buttons.close.disabled = true;
899
907
  const id = 'bottom-bar';
900
- if (options && options.homeModals && !options.homeModals.includes(id)) options.homeModals.push(id);
901
- else options.homeModals = [id];
908
+ if (!this.Data[idModal].homeModals) this.Data[idModal].homeModals = [];
909
+ if (!this.Data[idModal].homeModals.includes(id)) {
910
+ this.Data[idModal].homeModals.push(id);
911
+ }
902
912
  const html = async () => html`
903
913
  <style>
904
914
  .top-bar-search-box-container {
@@ -1048,13 +1058,80 @@ const Modal = {
1048
1058
 
1049
1059
  {
1050
1060
  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
- });
1061
+ // old method
1062
+ // EventsUI.onClick(`.action-btn-lang`, () => {
1063
+ // let lang = 'en';
1064
+ // if (s('html').lang === 'en') lang = 'es';
1065
+ // if (s(`.dropdown-option-${lang}`))
1066
+ // DropDown.Tokens['settings-lang'].onClickEvents[`dropdown-option-${lang}`]();
1067
+ // else Translate.renderLang(lang);
1068
+ // });
1069
+
1070
+ // Open lightweight empty modal on language button, with shared dismiss logic
1071
+ EventsUI.onClick(
1072
+ `.action-btn-lang`,
1073
+ async () => {
1074
+ const id = 'action-btn-lang-modal';
1075
+ if (s(`.${id}`)) {
1076
+ return s(`.btn-close-${id}`).click();
1077
+ }
1078
+ const { barConfig } = await Themes[Css.currentTheme]();
1079
+ barConfig.buttons.maximize.disabled = true;
1080
+ barConfig.buttons.minimize.disabled = true;
1081
+ barConfig.buttons.restore.disabled = true;
1082
+ barConfig.buttons.menu.disabled = true;
1083
+ barConfig.buttons.close.disabled = false;
1084
+ await Modal.Render({
1085
+ id,
1086
+ barConfig,
1087
+ title: html`${renderViewTitle({
1088
+ icon: html`<i class="fas fa-language mini-title"></i>`,
1089
+ text: Translate.Render('select lang'),
1090
+ })}`,
1091
+ html: async () => html`${await Translate.RenderSetting('action-drop-modal' + id)}`,
1092
+ titleClass: 'mini-title',
1093
+ style: {
1094
+ resize: 'none',
1095
+ width: '100% !important',
1096
+ height: '350px !important',
1097
+ 'max-width': '350px !important',
1098
+ 'z-index': 7,
1099
+ },
1100
+ dragDisabled: true,
1101
+ maximize: true,
1102
+ heightBottomBar: 0,
1103
+ heightTopBar: originHeightTopBar,
1104
+ barMode: options.barMode,
1105
+ });
1106
+
1107
+ // Move title inside the bar container to align with control buttons
1108
+ Modal.MoveTitleToBar(id);
1109
+
1110
+ // Position the language selection modal relative to the language button
1111
+ Modal.positionRelativeToAnchor({
1112
+ modalSelector: `.${id}`,
1113
+ anchorSelector: '.action-btn-lang',
1114
+ align: 'right',
1115
+ offset: { x: 0, y: 6 },
1116
+ autoVertical: true,
1117
+ });
1118
+
1119
+ // Hover/focus controller uses the button as input anchor
1120
+ const hoverFocusCtl = EventsUI.HoverFocusController({
1121
+ inputSelector: `.action-btn-lang`,
1122
+ panelSelector: `.${id}`,
1123
+ onDismiss: () => Modal.removeModal(id),
1124
+ });
1125
+ hoverFocusCtl.bind();
1126
+ const unbindDoc = EventsUI.bindDismissOnDocumentClick({
1127
+ shouldStay: hoverFocusCtl.shouldStay,
1128
+ onDismiss: () => Modal.removeModal(id),
1129
+ anchors: [`.action-btn-lang`, `.${id}`],
1130
+ });
1131
+ Modal.Data[id].onCloseListener[`unbind-doc-${id}`] = () => unbindDoc();
1132
+ },
1133
+ { context: 'modal', noGate: true, noLoading: true },
1134
+ );
1058
1135
  }
1059
1136
 
1060
1137
  {
@@ -1348,9 +1425,7 @@ const Modal = {
1348
1425
  this.onHomeRouterEvent = async () => {
1349
1426
  for (const keyModal of Object.keys(this.Data)) {
1350
1427
  if (
1351
- ![idModal, 'main-body-top', 'main-body']
1352
- .concat(options?.homeModals ? options.homeModals : [])
1353
- .includes(keyModal)
1428
+ ![idModal, 'main-body-top', 'main-body'].concat(this.Data[idModal]?.homeModals || []).includes(keyModal)
1354
1429
  )
1355
1430
  s(`.btn-close-${keyModal}`).click();
1356
1431
  backMenuButtonEvent();
@@ -1429,42 +1504,139 @@ const Modal = {
1429
1504
  default:
1430
1505
  break;
1431
1506
  }
1507
+ // Track drag position for consistency
1508
+ let dragPosition = { x: 0, y: 0 };
1509
+
1510
+ // Initialize drag options with proper bounds and smooth transitions
1432
1511
  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;
1512
+ handle: handle,
1513
+ bounds: {
1514
+ top: 0,
1515
+ left: 0,
1516
+ right: 0,
1517
+ bottom: 0,
1518
+ },
1519
+ preventDefault: true,
1520
+ position: { x: 0, y: 0 },
1521
+ onDragStart: () => {
1522
+ const modal = s(`.${idModal}`);
1523
+ if (!modal) return false; // Prevent drag if modal not found
1524
+
1525
+ // Store current position
1526
+ const computedStyle = window.getComputedStyle(modal);
1527
+ const matrix = new DOMMatrixReadOnly(computedStyle.transform);
1528
+ dragPosition = {
1529
+ x: matrix.m41 || 0,
1530
+ y: matrix.m42 || 0,
1531
+ };
1532
+
1533
+ modal.style.transition = 'none';
1534
+ modal.style.willChange = 'transform';
1535
+ return true; // Allow drag to start
1439
1536
  },
1440
1537
  onDrag: (data) => {
1441
- if (!s(`.${idModal}`)) return;
1442
- // logger.info('Dragging', data);
1538
+ // Update position based on drag delta
1539
+ dragPosition = { x: data.x, y: data.y };
1443
1540
  },
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
- );
1541
+ onDragEnd: () => {
1542
+ const modal = s(`.${idModal}`);
1543
+ if (!modal) return;
1544
+
1545
+ modal.style.willChange = '';
1546
+ modal.style.transition = transition;
1547
+
1548
+ // Update drag instance with current position
1549
+ if (dragInstance) {
1550
+ dragInstance.updateOptions({
1551
+ position: dragPosition,
1552
+ });
1553
+ }
1554
+
1555
+ // Notify listeners
1556
+ Object.keys(this.Data[idModal].onDragEndListener || {}).forEach((keyListener) => {
1557
+ this.Data[idModal].onDragEndListener[keyListener]?.();
1558
+ });
1451
1559
  },
1452
1560
  };
1453
- let dragInstance;
1454
- // new Draggable(s(`.${idModal}`), { disabled: true });
1455
- const setDragInstance = () => (options?.dragDisabled ? null : new Draggable(s(`.${idModal}`), dragOptions));
1561
+
1562
+ let dragInstance = null;
1563
+
1564
+ // Initialize or update drag instance
1565
+ const setDragInstance = () => {
1566
+ if (options?.dragDisabled) {
1567
+ if (dragInstance) {
1568
+ dragInstance.destroy();
1569
+ dragInstance = null;
1570
+ }
1571
+ return null;
1572
+ }
1573
+
1574
+ const modal = s(`.${idModal}`);
1575
+ if (!modal) {
1576
+ console.warn(`Modal element .${idModal} not found for drag initialization`);
1577
+ return null;
1578
+ }
1579
+
1580
+ // Ensure the modal has position: absolute for proper dragging
1581
+ if (window.getComputedStyle(modal).position !== 'absolute') {
1582
+ modal.style.position = 'absolute';
1583
+ }
1584
+
1585
+ // Clean up existing instance
1586
+ if (dragInstance) {
1587
+ dragInstance.destroy();
1588
+ }
1589
+
1590
+ try {
1591
+ // Create new instance with updated options
1592
+ dragInstance = new Draggable(modal, dragOptions);
1593
+ return dragInstance;
1594
+ } catch (error) {
1595
+ console.error('Failed to initialize draggable:', error);
1596
+ return null;
1597
+ }
1598
+ };
1599
+
1600
+ // Expose method to update drag options
1456
1601
  this.Data[idModal].setDragInstance = (updateDragOptions) => {
1457
- dragOptions = {
1458
- ...dragOptions,
1459
- ...updateDragOptions,
1460
- };
1602
+ if (updateDragOptions) {
1603
+ dragOptions = { ...dragOptions, ...updateDragOptions };
1604
+ }
1461
1605
  dragInstance = setDragInstance();
1462
1606
  this.Data[idModal].dragInstance = dragInstance;
1463
1607
  this.Data[idModal].dragOptions = dragOptions;
1464
1608
  };
1465
- s(`.${idModal}`).style.transition = '0.15s';
1466
- setTimeout(() => (s(`.${idModal}`).style.opacity = '1'));
1467
- setTimeout(() => (s(`.${idModal}`).style.transition = transition), 150);
1609
+ // Initialize modal with proper transitions
1610
+ const modal = s(`.${idModal}`);
1611
+ if (modal) {
1612
+ // Initial state
1613
+ modal.style.transition = 'opacity 0.15s ease, transform 0.3s ease';
1614
+ modal.style.opacity = '0';
1615
+
1616
+ // Trigger fade-in after a small delay to allow initial render
1617
+ requestAnimationFrame(() => {
1618
+ if (!modal) return;
1619
+ modal.style.opacity = '1';
1620
+
1621
+ // Set final transition after initial animation completes
1622
+ setTimeout(() => {
1623
+ if (modal) {
1624
+ modal.style.transition = transition;
1625
+
1626
+ // Initialize drag after transitions are set
1627
+ if (!options.dragDisabled) {
1628
+ setDragInstance();
1629
+ if (!options.mode) {
1630
+ dragInstance.updateOptions({
1631
+ position: { x: 0, y: 0 },
1632
+ disabled: false, // Ensure drag is enabled after restore
1633
+ });
1634
+ }
1635
+ }
1636
+ }
1637
+ }, 150);
1638
+ });
1639
+ }
1468
1640
 
1469
1641
  const btnCloseEvent = () => {
1470
1642
  Object.keys(this.Data[idModal].onCloseListener).map((keyListener) =>
@@ -1480,62 +1652,122 @@ const Modal = {
1480
1652
  setTimeout(() => {
1481
1653
  if (!s(`.${idModal}`)) return;
1482
1654
  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
- })();
1655
+ // Handle modal route change
1656
+ if (options.route) {
1657
+ closeModalRouteChangeEvent({
1658
+ route: options.route,
1659
+ RouterInstance: options.RouterInstance,
1660
+ homeCid: Modal.homeCid,
1661
+ });
1662
+ }
1504
1663
  }, 300);
1505
1664
  };
1506
1665
  s(`.btn-close-${idModal}`).onclick = btnCloseEvent;
1507
1666
 
1667
+ // Minimize button handler
1508
1668
  s(`.btn-minimize-${idModal}`).onclick = () => {
1509
- if (options.slideMenu) delete this.Data[idModal].slideMenu;
1510
- s(`.${idModal}`).style.transition = '0.3s';
1669
+ const modal = s(`.${idModal}`);
1670
+ if (!modal) return;
1671
+
1672
+ if (options.slideMenu) {
1673
+ delete this.Data[idModal].slideMenu;
1674
+ }
1675
+
1676
+ // Keep drag enabled when minimized
1677
+ if (dragInstance) {
1678
+ dragInstance.updateOptions({ disabled: false });
1679
+ }
1680
+
1681
+ // Set up transition
1682
+ modal.style.transition = 'height 0.3s ease, transform 0.3s ease';
1683
+
1684
+ // Update button states
1511
1685
  s(`.btn-minimize-${idModal}`).style.display = 'none';
1512
1686
  s(`.btn-maximize-${idModal}`).style.display = null;
1513
1687
  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);
1688
+
1689
+ // Collapse to header height
1690
+ const header = s(`.bar-default-modal-${idModal}`);
1691
+ if (header) {
1692
+ modal.style.height = `${header.clientHeight}px`;
1693
+ modal.style.overflow = 'hidden';
1694
+ }
1695
+
1696
+ // Restore transition after animation
1697
+ setTimeout(() => {
1698
+ if (modal) {
1699
+ modal.style.transition = transition;
1700
+ }
1701
+ }, 300);
1516
1702
  };
1703
+ // Restore button handler
1517
1704
  s(`.btn-restore-${idModal}`).onclick = () => {
1518
- if (options.slideMenu) delete this.Data[idModal].slideMenu;
1519
- s(`.${idModal}`).style.transition = '0.3s';
1705
+ const modal = s(`.${idModal}`);
1706
+ if (!modal) return;
1707
+
1708
+ if (options.slideMenu) {
1709
+ delete this.Data[idModal].slideMenu;
1710
+ }
1711
+
1712
+ // Re-enable dragging
1713
+ if (dragInstance) {
1714
+ dragInstance.updateOptions({ disabled: false });
1715
+ }
1716
+
1717
+ // Set up transition
1718
+ modal.style.transition = 'all 0.3s ease';
1719
+
1720
+ // Update button states
1520
1721
  s(`.btn-restore-${idModal}`).style.display = 'none';
1521
1722
  s(`.btn-minimize-${idModal}`).style.display = null;
1522
1723
  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);
1724
+
1725
+ // Restore original dimensions and position
1726
+ modal.style.transform = '';
1727
+ modal.style.height = '';
1728
+ left = 0;
1729
+ width = 300;
1730
+ modal.style.left = `${left}px`;
1731
+ modal.style.width = `${width}px`;
1732
+ modal.style.overflow = '';
1733
+
1734
+ // Reset drag position
1735
+ dragPosition = { x: 0, y: 0 };
1736
+
1737
+ // Set new position
1738
+ modal.style.transform = `translate(0, 0)`;
1739
+
1740
+ // Adjust top position based on top bar visibility
1741
+ const heightDefaultTopBar = 40; // Default top bar height if not specified
1742
+ s(`.${idModal}`).style.top = s(`.main-body-btn-ui-close`).classList.contains('hide')
1743
+ ? `0px`
1744
+ : `${options.heightTopBar ? options.heightTopBar : heightDefaultTopBar}px`;
1745
+
1746
+ // Re-enable drag after restore
1747
+ if (dragInstance) {
1748
+ dragInstance.updateOptions({
1749
+ position: { x: 0, y: 0 },
1750
+ disabled: false, // Ensure drag is enabled after restore
1751
+ });
1752
+ }
1753
+ setTimeout(() => (s(`.${idModal}`) ? (s(`.${idModal}`).style.transition = transition) : null), 300);
1531
1754
  };
1532
1755
  s(`.btn-maximize-${idModal}`).onclick = () => {
1533
- s(`.${idModal}`).style.transition = '0.3s';
1534
- setTimeout(() => (s(`.${idModal}`).style.transition = transition), 300);
1756
+ const modal = s(`.${idModal}`);
1757
+ if (!modal) return;
1758
+
1759
+ // Disable drag when maximizing
1760
+ if (dragInstance) {
1761
+ dragInstance.updateOptions({ disabled: true });
1762
+ }
1763
+
1764
+ modal.style.transition = '0.3s';
1765
+ setTimeout(() => (modal ? (modal.style.transition = transition) : null), 300);
1766
+
1535
1767
  s(`.btn-maximize-${idModal}`).style.display = 'none';
1536
1768
  s(`.btn-restore-${idModal}`).style.display = null;
1537
1769
  s(`.btn-minimize-${idModal}`).style.display = null;
1538
- s(`.${idModal}`).style.transform = null;
1770
+ modal.style.transform = null;
1539
1771
 
1540
1772
  if (options.slideMenu) {
1541
1773
  const idSlide = this.Data[options.slideMenu]['slide-menu']
@@ -1770,6 +2002,105 @@ const Modal = {
1770
2002
  };
1771
2003
  });
1772
2004
  },
2005
+ // Move modal title element into the bar's render container so it aligns with control buttons
2006
+ /**
2007
+ * Position a modal relative to an anchor element
2008
+ * @param {Object} options - Positioning options
2009
+ * @param {string} options.modalSelector - CSS selector for the modal element
2010
+ * @param {string} options.anchorSelector - CSS selector for the anchor element
2011
+ * @param {Object} [options.offset={x: 0, y: 6}] - Offset from anchor
2012
+ * @param {string} [options.align='right'] - Horizontal alignment ('left' or 'right')
2013
+ * @param {boolean} [options.autoVertical=true] - Whether to automatically determine vertical position
2014
+ * @param {boolean} [options.placeAbove] - Force position above/below anchor (overrides autoVertical)
2015
+ */
2016
+ positionRelativeToAnchor({
2017
+ modalSelector,
2018
+ anchorSelector,
2019
+ offset = { x: 0, y: 6 },
2020
+ align = 'right',
2021
+ autoVertical = true,
2022
+ placeAbove,
2023
+ }) {
2024
+ try {
2025
+ const modal = s(modalSelector);
2026
+ const anchor = s(anchorSelector);
2027
+
2028
+ if (!modal || !anchor || !anchor.getBoundingClientRect) return;
2029
+
2030
+ // First, position the modal near its final position but off-screen
2031
+ const arect = anchor.getBoundingClientRect();
2032
+ const vh = window.innerHeight;
2033
+ const vw = window.innerWidth;
2034
+ const safeMargin = 6;
2035
+
2036
+ // Determine vertical position
2037
+ let finalPlaceAbove = placeAbove;
2038
+ if (autoVertical && placeAbove === undefined) {
2039
+ const inBottomBar = anchor.closest && anchor.closest('.bottom-bar');
2040
+ const inTopBar = anchor.closest && anchor.closest('.slide-menu-top-bar');
2041
+
2042
+ if (inBottomBar) finalPlaceAbove = true;
2043
+ else if (inTopBar) finalPlaceAbove = false;
2044
+ else finalPlaceAbove = arect.top > vh / 2; // heuristic fallback
2045
+ }
2046
+
2047
+ // Set initial position (slightly offset from final position)
2048
+ const initialOffset = finalPlaceAbove ? 20 : -20;
2049
+ modal.style.position = 'fixed';
2050
+ modal.style.opacity = '0';
2051
+ modal.style.transition = 'opacity 150ms ease-out, transform 150ms ease-out';
2052
+
2053
+ // Position near the anchor but slightly offset
2054
+ modal.style.top = `${finalPlaceAbove ? arect.top - 40 : arect.bottom + 20}px`;
2055
+ modal.style.left = `${align === 'right' ? arect.right - 200 : arect.left}px`;
2056
+ modal.style.transform = 'translateY(0)';
2057
+
2058
+ // Force reflow to ensure initial styles are applied
2059
+ modal.offsetHeight;
2060
+
2061
+ // Now calculate final position
2062
+ const mrect = modal.getBoundingClientRect();
2063
+
2064
+ // Calculate final top position
2065
+ const top = finalPlaceAbove ? arect.top - mrect.height - offset.y : arect.bottom + offset.y;
2066
+
2067
+ // Calculate final left position based on alignment
2068
+ let left;
2069
+ if (align === 'right') {
2070
+ left = arect.right - mrect.width - offset.x; // align right edges
2071
+ } else {
2072
+ left = arect.left + offset.x; // align left edges
2073
+ }
2074
+
2075
+ // Ensure modal stays within viewport bounds
2076
+ left = Math.max(safeMargin, Math.min(left, vw - mrect.width - safeMargin));
2077
+ const finalTop = Math.max(safeMargin, Math.min(top, vh - mrect.height - safeMargin));
2078
+
2079
+ // Apply final position with smooth transition
2080
+ requestAnimationFrame(() => {
2081
+ modal.style.top = `${Math.round(finalTop)}px`;
2082
+ modal.style.left = `${Math.round(left)}px`;
2083
+ modal.style.opacity = '1';
2084
+ });
2085
+ } catch (e) {
2086
+ console.error('Error positioning modal:', e);
2087
+ }
2088
+ },
2089
+
2090
+ MoveTitleToBar(idModal) {
2091
+ try {
2092
+ const titleEl = s(`.title-modal-${idModal}`);
2093
+ const container = s(`.btn-bar-modal-container-render-${idModal}`);
2094
+ if (!titleEl || !container) return;
2095
+ const titleNode = titleEl.cloneNode(true);
2096
+ titleEl.remove();
2097
+ container.classList.add('in');
2098
+ container.classList.add('fll');
2099
+ container.appendChild(titleNode);
2100
+ } catch (e) {
2101
+ // non-fatal: keep default placement if structure not present
2102
+ }
2103
+ },
1773
2104
  headerTitleHeight: 40,
1774
2105
  actionBtnCenter: function () {
1775
2106
  if (!s(`.btn-close-modal-menu`).classList.contains('hide')) {