@bobfrankston/mailx 1.0.192 → 1.0.194

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.
@@ -1 +1 @@
1
- {"height":1344,"width":2151,"x":500,"y":36}
1
+ {"height":1344,"width":2151,"x":400,"y":28}
package/client/app.js CHANGED
@@ -346,33 +346,53 @@ async function openCompose(mode) {
346
346
  showComposeOverlay();
347
347
  }
348
348
  function showComposeOverlay() {
349
- // Wrapper div for iframe + close button
350
349
  const wrapper = document.createElement("div");
351
350
  wrapper.className = "compose-overlay";
352
- wrapper.style.cssText = "position:fixed;bottom:0;right:16px;width:min(900px,55vw);height:min(700px,70vh);z-index:1000;border-radius:8px 8px 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.3);display:flex;flex-direction:column;";
353
- // Close button
351
+ wrapper.style.cssText = "position:fixed;bottom:0;right:16px;width:min(900px,55vw);height:min(700px,70vh);z-index:1000;border-radius:8px 8px 0 0;box-shadow:0 -4px 24px rgba(0,0,0,0.3);display:flex;flex-direction:column;resize:both;overflow:hidden;";
352
+ // Title bar — drag to move, close button
353
+ const titleBar = document.createElement("div");
354
+ titleBar.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:#e8ecf0;border-radius:8px 8px 0 0;cursor:move;user-select:none;flex-shrink:0;";
355
+ titleBar.textContent = "Compose";
354
356
  const closeBtn = document.createElement("button");
355
357
  closeBtn.textContent = "✕";
356
- closeBtn.title = "Close compose";
357
- closeBtn.style.cssText = "position:absolute;top:4px;right:8px;z-index:1001;background:none;border:none;font-size:18px;cursor:pointer;color:#666;padding:2px 6px;border-radius:4px;";
358
+ closeBtn.title = "Save draft and close";
359
+ closeBtn.style.cssText = "background:none;border:none;font-size:16px;cursor:pointer;color:#666;padding:2px 6px;border-radius:4px;";
358
360
  closeBtn.addEventListener("mouseenter", () => closeBtn.style.color = "#c00");
359
361
  closeBtn.addEventListener("mouseleave", () => closeBtn.style.color = "#666");
360
362
  closeBtn.addEventListener("click", () => {
361
- // Trigger draft save in the iframe before closing
362
363
  try {
363
364
  const win = frame.contentWindow;
364
- if (win) {
365
- // Dispatch a custom event that compose.ts can listen to
365
+ if (win)
366
366
  win.dispatchEvent(new Event("compose-save-and-close"));
367
- }
368
367
  }
369
- catch { /* cross-origin */ }
370
- // Give the draft save a moment, then close
368
+ catch { /* */ }
371
369
  setTimeout(() => wrapper.remove(), 500);
372
370
  });
371
+ titleBar.appendChild(closeBtn);
372
+ // Drag to move
373
+ let dragX = 0, dragY = 0;
374
+ titleBar.addEventListener("mousedown", (e) => {
375
+ if (e.target === closeBtn)
376
+ return;
377
+ e.preventDefault();
378
+ dragX = e.clientX - wrapper.offsetLeft;
379
+ dragY = e.clientY - wrapper.offsetTop;
380
+ const onMove = (e) => {
381
+ wrapper.style.left = `${e.clientX - dragX}px`;
382
+ wrapper.style.top = `${e.clientY - dragY}px`;
383
+ wrapper.style.bottom = "auto";
384
+ wrapper.style.right = "auto";
385
+ };
386
+ const onUp = () => {
387
+ document.removeEventListener("mousemove", onMove);
388
+ document.removeEventListener("mouseup", onUp);
389
+ };
390
+ document.addEventListener("mousemove", onMove);
391
+ document.addEventListener("mouseup", onUp);
392
+ });
373
393
  const frame = document.createElement("iframe");
374
394
  frame.src = "compose/compose.html";
375
- frame.style.cssText = "flex:1;border:none;border-radius:8px 8px 0 0;background:#fff;width:100%;";
395
+ frame.style.cssText = "flex:1;border:none;background:#fff;width:100%;";
376
396
  // Close when compose calls window.close()
377
397
  frame.addEventListener("load", () => {
378
398
  try {
@@ -383,7 +403,12 @@ function showComposeOverlay() {
383
403
  }
384
404
  catch { /* cross-origin safety */ }
385
405
  });
386
- wrapper.appendChild(closeBtn);
406
+ // Bring to front on click
407
+ wrapper.addEventListener("mousedown", () => {
408
+ document.querySelectorAll(".compose-overlay").forEach(el => el.style.zIndex = "1000");
409
+ wrapper.style.zIndex = "1001";
410
+ });
411
+ wrapper.appendChild(titleBar);
387
412
  wrapper.appendChild(frame);
388
413
  document.body.appendChild(wrapper);
389
414
  }
@@ -44,8 +44,10 @@ export function showContextMenu(x, y, items) {
44
44
  menu.style.top = `${y - rect.height}px`;
45
45
  activeMenu = menu;
46
46
  }
47
- // Close on click outside or Escape
47
+ // Close on any interaction outside the menu
48
48
  document.addEventListener("click", closeContextMenu);
49
49
  document.addEventListener("keydown", (e) => { if (e.key === "Escape")
50
50
  closeContextMenu(); });
51
+ document.addEventListener("scroll", closeContextMenu, true);
52
+ document.addEventListener("contextmenu", () => { });
51
53
  //# sourceMappingURL=context-menu.js.map
@@ -3,6 +3,7 @@
3
3
  * Subscribes to message-state: clears when selected becomes null.
4
4
  */
5
5
  import { getMessage, updateFlags, allowRemoteContent, getAttachment } from "../lib/api-client.js";
6
+ import { showContextMenu } from "./context-menu.js";
6
7
  import * as state from "../lib/message-state.js";
7
8
  /** Currently displayed message (for reply/forward) */
8
9
  let currentMessage = null;
@@ -58,7 +59,7 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
58
59
  const bodyEl = document.getElementById("mv-body");
59
60
  const attEl = document.getElementById("mv-attachments");
60
61
  bodyEl.innerHTML = `<div class="mv-empty">Fetching message body...</div>`;
61
- // Don't hide the header — keep previous header visible until new one loads
62
+ headerEl.hidden = true;
62
63
  attEl.hidden = true;
63
64
  try {
64
65
  const msg = await getMessage(accountId, uid, false, folderId);
@@ -74,12 +75,34 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
74
75
  }
75
76
  // Header
76
77
  headerEl.hidden = false;
77
- headerEl.querySelector(".mv-from").textContent = formatAddr(msg.from);
78
- headerEl.querySelector(".mv-to").textContent = `To: ${msg.to.map(formatAddr).join(", ")}`;
78
+ const fromEl = headerEl.querySelector(".mv-from");
79
+ const toEl = headerEl.querySelector(".mv-to");
80
+ fromEl.textContent = formatAddr(msg.from);
81
+ toEl.textContent = `To: ${msg.to.map(formatAddr).join(", ")}`;
79
82
  if (msg.cc?.length) {
80
- headerEl.querySelector(".mv-to").textContent += ` Cc: ${msg.cc.map(formatAddr).join(", ")}`;
83
+ toEl.textContent += ` Cc: ${msg.cc.map(formatAddr).join(", ")}`;
81
84
  }
82
85
  headerEl.querySelector(".mv-subject").textContent = msg.subject;
86
+ // Right-click on email addresses in header
87
+ const allAddresses = [msg.from, ...(msg.to || []), ...(msg.cc || [])].filter((a) => a?.address);
88
+ for (const el of [fromEl, toEl]) {
89
+ el.addEventListener("contextmenu", (e) => {
90
+ e.preventDefault();
91
+ const me = e;
92
+ const items = [];
93
+ for (const addr of (el === fromEl ? [msg.from] : [...(msg.to || []), ...(msg.cc || [])])) {
94
+ if (!addr?.address)
95
+ continue;
96
+ const display = addr.name ? `${addr.name} <${addr.address}>` : addr.address;
97
+ items.push({ label: `Copy: ${display}`, action: () => navigator.clipboard.writeText(addr.address) });
98
+ }
99
+ items.push({ label: "", action: () => { }, separator: true });
100
+ items.push({ label: "Reply", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })) });
101
+ items.push({ label: "Reply All", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })) });
102
+ items.push({ label: "Forward", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })) });
103
+ showContextMenu(me.clientX, me.clientY, items);
104
+ });
105
+ }
83
106
  headerEl.querySelector(".mv-date").textContent = new Date(msg.date).toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false });
84
107
  // Unsubscribe button (upper right of header)
85
108
  const unsubBtn = document.getElementById("mv-unsubscribe");
@@ -155,7 +155,7 @@ body {
155
155
  }
156
156
  }
157
157
 
158
- /* Hide hamburger and back on wide screens */
159
- @media (min-width: 769px) {
158
+ /* Hide hamburger and back on wide screens (folder panel always visible) */
159
+ @media (min-width: 1101px) {
160
160
  #btn-menu, #btn-back { display: none !important; }
161
161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.192",
3
+ "version": "1.0.194",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -24,7 +24,7 @@
24
24
  "@bobfrankston/iflow-node": "^0.1.2",
25
25
  "@bobfrankston/miscinfo": "^1.0.8",
26
26
  "@bobfrankston/oauthsupport": "^1.0.21",
27
- "@bobfrankston/msger": "^0.1.243",
27
+ "@bobfrankston/msger": "^0.1.245",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",