From cdcc199e2ecf2a726ba451b994cd242850da8397 Mon Sep 17 00:00:00 2001 From: master3395 Date: Thu, 26 Mar 2026 00:26:27 +0100 Subject: [PATCH] webmail: account picker search, list/reader resize, assets v28 - Searchable mailbox dropdown and compose From filter - Resizable split between message list and reader pane - Styles for picker, list-detail resizer, sr-only; bump webmail.js to v28 --- webmail/static/webmail/webmail.css | 144 +++++++++++++++++++++- webmail/static/webmail/webmail.js | 175 ++++++++++++++++++++++++++- webmail/templates/webmail/index.html | 63 +++++++--- 3 files changed, 363 insertions(+), 19 deletions(-) diff --git a/webmail/static/webmail/webmail.css b/webmail/static/webmail/webmail.css index da7953a6d..d33db7630 100644 --- a/webmail/static/webmail/webmail.css +++ b/webmail/static/webmail/webmail.css @@ -44,6 +44,120 @@ cursor: pointer; } +.wm-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.wm-account-switch { + position: relative; +} + +.wm-account-picker-trigger { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 14px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: border-color 0.15s ease, background 0.15s ease; +} + +.wm-account-picker-trigger:hover { + border-color: var(--accent-color); + background: var(--bg-secondary); +} + +.wm-account-picker-chevron { + font-size: 11px; + opacity: 0.65; + transition: transform 0.2s ease; +} + +.wm-account-picker-chevron.is-open { + transform: rotate(180deg); +} + +.wm-account-picker-dropdown { + position: absolute; + right: 0; + top: calc(100% + 6px); + min-width: min(360px, 92vw); + max-width: 420px; + max-height: min(340px, 70vh); + display: flex; + flex-direction: column; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 10px; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18); + z-index: 200; + overflow: hidden; +} + +.wm-account-search-input { + margin: 10px 10px 6px; + flex-shrink: 0; + font-size: 13px; +} + +.wm-account-picker-list { + list-style: none; + margin: 0; + padding: 4px 0; + overflow-y: auto; + flex: 1; + max-height: 260px; +} + +.wm-account-picker-item { + padding: 8px 14px; + font-size: 13px; + color: var(--text-primary); + cursor: pointer; + word-break: break-all; + line-height: 1.35; + border-left: 3px solid transparent; +} + +.wm-account-picker-item:hover { + background: var(--bg-secondary); +} + +.wm-account-picker-item.is-active { + background: rgba(108, 92, 231, 0.1); + border-left-color: var(--accent-color); + font-weight: 600; +} + +.wm-account-picker-empty { + padding: 14px; + font-size: 13px; + color: var(--text-secondary); + text-align: center; +} + +.wm-compose-account-filter { + margin-bottom: 8px; + font-size: 13px; +} + +.wm-compose-account-select { + margin-top: 0; +} + /* Main Layout */ .wm-layout { display: flex; @@ -85,12 +199,35 @@ outline: none; } -body.wm-resizing-sidebar { +/* Drag handle between message list and reader (same interaction as folder panel resizer) */ +.wm-list-detail-resizer { + width: 6px; + flex-shrink: 0; + cursor: col-resize; + background: transparent; + border-right: 1px solid var(--border-color); + align-self: stretch; + margin: 0 -3px 0 -3px; + padding: 0; + z-index: 5; + touch-action: none; + transition: background 0.15s ease; +} + +.wm-list-detail-resizer:hover, +.wm-list-detail-resizer:focus { + background: rgba(108, 92, 231, 0.18); + outline: none; +} + +body.wm-resizing-sidebar, +body.wm-resizing-message-list { cursor: col-resize !important; user-select: none !important; } -body.wm-resizing-sidebar * { +body.wm-resizing-sidebar *, +body.wm-resizing-message-list * { cursor: col-resize !important; } @@ -1054,7 +1191,8 @@ button.wm-nav-link.wm-nav-btn:focus { .wm-layout { flex-direction: column; } - .wm-sidebar-resizer { + .wm-sidebar-resizer, + .wm-list-detail-resizer { display: none !important; } .wm-sidebar { diff --git a/webmail/static/webmail/webmail.js b/webmail/static/webmail/webmail.js index 37417e009..aed178c17 100644 --- a/webmail/static/webmail/webmail.js +++ b/webmail/static/webmail/webmail.js @@ -328,6 +328,44 @@ app.directive('wmSidebarResizerTouch', function() { }; }); +app.directive('wmListDetailResizerTouch', function() { + return { + restrict: 'A', + link: function(scope, element) { + element.on('touchstart', function(ev) { + try { + ev.preventDefault(); + } catch (e) { /* ignore */ } + scope.startMessageListResize(ev); + }); + scope.$on('$destroy', function() { + element.off('touchstart'); + }); + } + }; +}); + +/** Focus input when expression becomes true (e.g. open account picker). No isolate scope (same element may use ng-model). */ +app.directive('wmFocusWhen', ['$timeout', function($timeout) { + return { + restrict: 'A', + link: function(scope, element, attrs) { + scope.$watch(attrs.wmFocusWhen, function(v) { + if (v) { + $timeout(function() { + try { + element[0].focus(); + if (typeof element[0].select === 'function') { + element[0].select(); + } + } catch (e) { /* ignore */ } + }, 0); + } + }); + } + }; +}]); + app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', '$document', '$window', function($scope, $http, $sce, $timeout, $document, $window) { // System folders: must stay in sync with webmailManager.apiDeleteFolder protected set @@ -337,6 +375,14 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', '$document var WM_SIDEBAR_DEFAULT = 220; var sidebarResizeActive = false; + var WM_MESSAGE_LIST_WIDTH_KEY = 'wm_message_list_width_px'; + var WM_ML_MIN = 240; + var WM_ML_MAX = 720; + var WM_ML_DEFAULT = 380; + /** Minimum width (px) kept for the message reader / detail column when dragging the list split. */ + var WM_DETAIL_MIN = 280; + var listDetailResizeActive = false; + var WM_FOLDER_PROTECTED = { 'INBOX': true, 'INBOX.Sent': true, 'INBOX.Drafts': true, 'INBOX.Deleted Items': true, @@ -348,6 +394,11 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', '$document // ── State ──────────────────────────────────────────────── $scope.currentEmail = ''; $scope.managedAccounts = []; + $scope.accountPickerOpen = false; + $scope.accountPickerQuery = ''; + $scope.composeAccountFilter = ''; + /** Bound click handler on document.body to close account picker; cleared on close/destroy. */ + var accountPickerBodyClose = null; $scope.folders = []; $scope.displayFolders = []; /** Nested sidebar rows: { folder, depth, hasChildren } */ @@ -372,12 +423,17 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', '$document $scope.selectAll = false; $scope.sidebarWidthPx = WM_SIDEBAR_DEFAULT; $scope.sidebarResizeEnabled = true; + $scope.messageListWidthPx = WM_ML_DEFAULT; + $scope.listDetailResizeEnabled = true; function refreshSidebarResizeEnabled() { try { - $scope.sidebarResizeEnabled = $window.matchMedia('(min-width: 769px)').matches; + var wide = $window.matchMedia('(min-width: 769px)').matches; + $scope.sidebarResizeEnabled = wide; + $scope.listDetailResizeEnabled = wide; } catch (e) { $scope.sidebarResizeEnabled = true; + $scope.listDetailResizeEnabled = true; } } refreshSidebarResizeEnabled(); @@ -389,8 +445,37 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', '$document } } catch (e) { /* ignore */ } + try { + var storedMl = parseInt(localStorage.getItem(WM_MESSAGE_LIST_WIDTH_KEY), 10); + if (!isNaN(storedMl) && storedMl >= WM_ML_MIN && storedMl <= WM_ML_MAX) { + $scope.messageListWidthPx = storedMl; + } + } catch (e) { /* ignore */ } + + function maxMessageListWidthAllowed() { + try { + var side = ($scope.sidebarWidthPx != null) ? $scope.sidebarWidthPx : WM_SIDEBAR_DEFAULT; + var resizerGutter = 18; + return Math.max(WM_ML_MIN, $window.innerWidth - side - resizerGutter - WM_DETAIL_MIN); + } catch (e) { + return WM_ML_MAX; + } + } + + function clampMessageListWidth(n) { + var w = Math.round(Number(n)); + if (isNaN(w)) w = WM_ML_DEFAULT; + var cap = Math.min(WM_ML_MAX, maxMessageListWidthAllowed()); + return Math.max(WM_ML_MIN, Math.min(cap, w)); + } + + $scope.messageListWidthPx = clampMessageListWidth($scope.messageListWidthPx); + angular.element($window).on('resize', function() { - $scope.$applyAsync(refreshSidebarResizeEnabled); + $scope.$applyAsync(function() { + refreshSidebarResizeEnabled(); + $scope.messageListWidthPx = clampMessageListWidth($scope.messageListWidthPx); + }); }); $scope.startSidebarResize = function(event) { @@ -422,6 +507,7 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', '$document try { localStorage.setItem(WM_SIDEBAR_WIDTH_KEY, String($scope.sidebarWidthPx)); } catch (err) { /* ignore */ } + $scope.messageListWidthPx = clampMessageListWidth($scope.messageListWidthPx); angular.element($window.document.body).removeClass('wm-resizing-sidebar'); } @@ -429,6 +515,40 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', '$document $document.on('mousemove touchmove', onMove); $document.on('mouseup touchend touchcancel', onUp); }; + + $scope.startMessageListResize = function(event) { + if (!$scope.listDetailResizeEnabled) return; + if (listDetailResizeActive) return; + if (event.type === 'mousedown' && event.button !== 0) return; + event.preventDefault(); + listDetailResizeActive = true; + var startX = event.clientX != null ? event.clientX : (event.originalEvent && event.originalEvent.touches && event.originalEvent.touches[0] ? event.originalEvent.touches[0].clientX : (event.touches && event.touches[0] ? event.touches[0].clientX : 0)); + var startW = $scope.messageListWidthPx; + + function onMove(e) { + if (!$scope.listDetailResizeEnabled) return; + var oe = e.originalEvent || e; + var x = oe.clientX != null ? oe.clientX : (oe.touches && oe.touches[0] ? oe.touches[0].clientX : (e.clientX != null ? e.clientX : startX)); + var dx = x - startX; + $scope.messageListWidthPx = clampMessageListWidth(Math.round(startW + dx)); + $scope.$digest(); + } + + function onUp() { + listDetailResizeActive = false; + $document.off('mousemove touchmove', onMove); + $document.off('mouseup touchend touchcancel', onUp); + try { + localStorage.setItem(WM_MESSAGE_LIST_WIDTH_KEY, String($scope.messageListWidthPx)); + } catch (err) { /* ignore */ } + angular.element($window.document.body).removeClass('wm-resizing-message-list'); + } + + angular.element($window.document.body).addClass('wm-resizing-message-list'); + $document.on('mousemove touchmove', onMove); + $document.on('mouseup touchend touchcancel', onUp); + }; + $scope.showMoveDropdown = false; $scope.moveTarget = ''; $scope.showBcc = false; @@ -536,6 +656,57 @@ app.controller('webmailCtrl', ['$scope', '$http', '$sce', '$timeout', '$document }); }; + function detachAccountPickerBodyListener() { + if (accountPickerBodyClose) { + angular.element($window.document.body).off('click', accountPickerBodyClose); + accountPickerBodyClose = null; + } + } + + $scope.toggleAccountPicker = function(ev) { + if (ev) ev.stopPropagation(); + if ($scope.accountPickerOpen) { + $scope.accountPickerOpen = false; + $scope.accountPickerQuery = ''; + detachAccountPickerBodyListener(); + return; + } + $scope.accountPickerOpen = true; + $scope.accountPickerQuery = ''; + $timeout(function() { + accountPickerBodyClose = function() { + $scope.$apply(function() { + $scope.accountPickerOpen = false; + $scope.accountPickerQuery = ''; + }); + detachAccountPickerBodyListener(); + }; + angular.element($window.document.body).on('click', accountPickerBodyClose); + }, 0); + }; + + $scope.selectManagedAccount = function(email) { + var addr = (email || '').trim(); + if (!addr) { + $scope.accountPickerOpen = false; + $scope.accountPickerQuery = ''; + detachAccountPickerBodyListener(); + return; + } + $scope.accountPickerOpen = false; + $scope.accountPickerQuery = ''; + detachAccountPickerBodyListener(); + if (addr === ($scope.currentEmail || '').trim()) { + return; + } + $scope.currentEmail = addr; + $scope.switchAccount(); + }; + + $scope.$on('$destroy', function() { + detachAccountPickerBodyListener(); + }); + // ── Account Switching ──────────────────────────────────── $scope.switchAccount = function() { var newEmail = $scope.currentEmail; diff --git a/webmail/templates/webmail/index.html b/webmail/templates/webmail/index.html index 513ae8db5..8bc5df969 100644 --- a/webmail/templates/webmail/index.html +++ b/webmail/templates/webmail/index.html @@ -2,11 +2,6 @@ {% load i18n %} {% load static %} {% block title %}{% trans "Webmail - CyberPanel" %}{% endblock %} -{% block header_scripts %} -{{ block.super }} - - -{% endblock %} {% block content %} @@ -34,10 +29,35 @@ {$ currentEmail $} -
- +
@@ -136,7 +156,8 @@ -
+
@@ -502,7 +537,7 @@
-

{% trans "Settings" %} (UI build 2026-03-26 · Spam-only map · Sent row)

+

{% trans "Settings" %}

@@ -637,6 +672,6 @@
- + {% endblock %}