diff --git a/static/js/document.js b/static/js/document.js index cc5af1f..ae7aa19 100644 --- a/static/js/document.js +++ b/static/js/document.js @@ -2824,8 +2824,9 @@ import * as Modals from './modalManager.js'; onAction: () => { canceled = true; }, }); } - detachedEmailDoc = _detachActiveEmailForBackground(sendDocId); - await _sleep(1200); + await _sleep(1000); + if (!canceled) detachedEmailDoc = _detachActiveEmailForBackground(sendDocId); + await _sleep(200); if (canceled) { _restoreDetachedEmailDoc(detachedEmailDoc); detachedEmailDoc = null; @@ -2837,6 +2838,7 @@ import * as Modals from './modalManager.js'; if (uiModule) { uiModule.showToast('Message sent', { duration: 2200, + leadingIcon: 'check', action: 'Undo', actionHint: 'undo send', onAction: () => { undone = true; }, @@ -2868,6 +2870,7 @@ import * as Modals from './modalManager.js'; if (uiModule) { uiModule.showToast('Message sent', { duration: 7000, + leadingIcon: 'check', action: 'View Message', onAction: () => { import('./emailLibrary.js').then(mod => { diff --git a/static/js/ui.js b/static/js/ui.js index 5af1d2c..dacd804 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -8,11 +8,41 @@ import themeModule from './theme.js'; let toastEl = null; let autoScrollEnabled = true; +let hoveredToggleCard = null; // Smooth scroll state let _scrollRafId = null; let _scrollBox = null; +function _isTextEditingTarget(target) { + const el = target && target.nodeType === 1 ? target : target?.parentElement; + return !!(el && el.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]')); +} + +function _initHoverCardSpaceToggle() { + if (document._odysseusHoverCardSpaceToggle) return; + document._odysseusHoverCardSpaceToggle = true; + document.addEventListener('pointerover', (e) => { + const card = e.target?.closest?.('#email-lib-modal .doclib-card, #doclib-modal .doclib-card, .email-reader-tab-modal .doclib-card, .email-window-modal .doclib-card'); + if (card) hoveredToggleCard = card; + }, true); + document.addEventListener('pointerout', (e) => { + if (!hoveredToggleCard) return; + const next = e.relatedTarget; + if (!next || !hoveredToggleCard.contains(next)) hoveredToggleCard = null; + }, true); + document.addEventListener('keydown', (e) => { + if (e.code !== 'Space' || e.repeat || !hoveredToggleCard || !document.contains(hoveredToggleCard)) return; + if (_isTextEditingTarget(e.target)) return; + const blocked = e.target?.closest?.('button, a, input, textarea, select, [contenteditable="true"], [contenteditable=""], .recipient-chip, .doclib-card-dropdown, .email-card-dropdown'); + if (blocked) return; + e.preventDefault(); + hoveredToggleCard.click(); + }, true); +} + +_initHoverCardSpaceToggle(); + /** * Copy text to clipboard */ @@ -104,18 +134,25 @@ export function showToast(msg, durationOrOpts) { toastEl.textContent = ''; toastEl.classList.remove('error'); - let duration = 1200, actionLabel = null, onAction = null, actionHint = null, actionIcon = null; + let duration = 1200, actionLabel = null, onAction = null, actionHint = null, actionIcon = null, leadingIcon = null; if (typeof durationOrOpts === 'object' && durationOrOpts) { duration = durationOrOpts.duration || 5000; actionLabel = durationOrOpts.action; onAction = durationOrOpts.onAction; actionHint = durationOrOpts.actionHint || null; actionIcon = durationOrOpts.actionIcon || null; + leadingIcon = durationOrOpts.leadingIcon || null; } else if (typeof durationOrOpts === 'number') { duration = durationOrOpts; } const textSpan = document.createElement('span'); + if (leadingIcon === 'check') { + const icon = document.createElement('span'); + icon.className = 'toast-checkmark'; + icon.innerHTML = ''; + toastEl.appendChild(icon); + } textSpan.textContent = msg; toastEl.appendChild(textSpan); diff --git a/static/style.css b/static/style.css index dafeebd..b0fd1a7 100644 --- a/static/style.css +++ b/static/style.css @@ -3534,6 +3534,32 @@ body.bg-pattern-sparkles { max-width: min(360px, calc(100vw - 32px)); } .toast.show { opacity:1; transform: translateX(0); } + .toast .toast-checkmark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + margin-right: 7px; + color: var(--green, #50fa7b); + vertical-align: -3px; + transform: scale(0.65); + opacity: 0; + animation: toastCheckPop 360ms cubic-bezier(0.2, 0.9, 0.25, 1.25) forwards; + } + .toast .toast-checkmark svg polyline { + stroke-dasharray: 24; + stroke-dashoffset: 24; + animation: toastCheckDraw 420ms ease-out 120ms forwards; + } + @keyframes toastCheckPop { + 0% { opacity: 0; transform: scale(0.65); } + 65% { opacity: 1; transform: scale(1.16); } + 100% { opacity: 1; transform: scale(1); } + } + @keyframes toastCheckDraw { + to { stroke-dashoffset: 0; } + } .toast.exiting { opacity: 0; transform: translateX(-120%); @@ -10654,6 +10680,8 @@ textarea.memory-add-input { flex: 1; min-width: 0; max-width: 70vw; + container-type: inline-size; + container-name: docpane; display: flex; flex-direction: column; background: var(--bg); @@ -14210,6 +14238,30 @@ body.left-dock-active { #doclib-modal.doclib-fullscreen .doclib-modal-content { transition: none !important; } +.modal.modal-right-docked .email-reader-header, +.modal.modal-left-docked .email-reader-header { + flex-direction: column; + gap: 6px; +} +.modal.modal-right-docked .email-reader-actions, +.modal.modal-left-docked .email-reader-actions { + align-self: flex-end; +} +.modal.modal-right-docked .email-reader-meta-row, +.modal.modal-left-docked .email-reader-meta-row { + display: grid; + grid-template-columns: 1fr; + gap: 2px; + align-items: start; +} +.modal.modal-right-docked .email-reader-meta-row strong, +.modal.modal-left-docked .email-reader-meta-row strong { + min-width: 0; +} +.modal.modal-right-docked .recipient-chip, +.modal.modal-left-docked .recipient-chip { + max-width: 100%; +} .archive-list { margin-top: 8px; border-top: 1px solid var(--border); @@ -26101,6 +26153,27 @@ button .spinner-whirlpool { border-color: var(--accent-primary, var(--red)); max-width: 500px; } +@container docpane (max-width: 460px) { + .email-reader-header { + flex-direction: column; + gap: 6px; + } + .email-reader-actions { + align-self: flex-end; + } + .email-reader-meta-row { + display: grid; + grid-template-columns: 1fr; + gap: 2px; + align-items: start; + } + .email-reader-meta-row strong { + min-width: 0; + } + .recipient-chip { + max-width: 100%; + } +} .email-reader-actions { display: flex; gap: 4px; flex-wrap: nowrap; align-items: center; flex-shrink: 0; @@ -27564,6 +27637,21 @@ body.doc-find-active mark.doc-find-mark.current { min-width: 0; } .email-field input:focus { border-color: var(--accent, #4a9eff); } +@container docpane (max-width: 460px) { + .doc-email-header .email-field { + display: grid; + grid-template-columns: 1fr; + gap: 3px; + align-items: stretch; + } + .doc-email-header .email-field label { + min-width: 0; + text-align: left; + } + .doc-email-header .email-field input { + width: 100%; + } +} /* Cc toggle and attach button are absolute so they don't steal width from the To input */ .email-field .email-cc-toggle { position: absolute; right: 6px; top: 50%; transform: translateY(-50%);