@bobfrankston/mailx 1.0.42 → 1.0.44

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.
package/client/app.js CHANGED
@@ -110,11 +110,40 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
110
110
  });
111
111
  initMessageList((accountId, uid, folderId) => {
112
112
  showMessage(accountId, uid, folderId, currentFolderSpecialUse);
113
- // Enable action buttons when a message is selected
114
- for (const id of ["btn-reply", "btn-reply-all", "btn-forward", "btn-delete", "btn-flag"]) {
115
- const btn = document.getElementById(id);
116
- if (btn)
117
- btn.disabled = false;
113
+ // Narrow screen: show message viewer, hide list
114
+ if (window.innerWidth <= 768) {
115
+ document.getElementById("message-viewer")?.classList.add("narrow-active");
116
+ document.getElementById("message-list")?.classList.add("narrow-hidden");
117
+ }
118
+ });
119
+ // ── Auto two-line when message list is narrow ──
120
+ const messageList = document.getElementById("message-list");
121
+ if (messageList) {
122
+ const twoLineThreshold = 600; // px — switch to two-line below this width
123
+ const userTwoLine = localStorage.getItem("mailx-two-line") === "true";
124
+ new ResizeObserver(([entry]) => {
125
+ const narrow = entry.contentRect.width < twoLineThreshold;
126
+ // Auto two-line when narrow, respect user preference when wide
127
+ if (narrow) {
128
+ messageList.classList.add("two-line");
129
+ }
130
+ else if (!userTwoLine) {
131
+ messageList.classList.remove("two-line");
132
+ }
133
+ }).observe(messageList);
134
+ }
135
+ // ── Narrow screen navigation ──
136
+ document.getElementById("btn-menu")?.addEventListener("click", () => {
137
+ document.querySelector(".folder-panel")?.classList.toggle("open");
138
+ });
139
+ document.getElementById("btn-back")?.addEventListener("click", () => {
140
+ document.getElementById("message-viewer")?.classList.remove("narrow-active");
141
+ document.getElementById("message-list")?.classList.remove("narrow-hidden");
142
+ });
143
+ // Close folder panel when a folder is selected (narrow mode)
144
+ document.getElementById("folder-tree")?.addEventListener("click", (e) => {
145
+ if (window.innerWidth <= 768 && e.target.closest(".ft-folder")) {
146
+ document.querySelector(".folder-panel")?.classList.remove("open");
118
147
  }
119
148
  });
120
149
  // ── Toolbar actions ──
@@ -585,8 +614,9 @@ optFlagged?.addEventListener("change", () => {
585
614
  const isApp = typeof mailxapi !== "undefined" && mailxapi?.isApp;
586
615
  fetch("/api/version").then(r => r.json()).then(d => {
587
616
  const el = document.getElementById("app-version");
617
+ const driveName = d.drive === "local" ? "" : ` [${d.drive?.split(/[/\\]/).pop() || d.drive}]`;
588
618
  if (el)
589
- el.textContent = `mailx s${d.server}/c${d.client}${isApp ? "" : " [browser]"}`;
619
+ el.textContent = `mailx s${d.server}/c${d.client}${driveName}${isApp ? "" : " [browser]"}`;
590
620
  }).catch(async () => {
591
621
  // Server not running — try to start it if we're in the app
592
622
  const startupStatus = document.getElementById("startup-status");
@@ -639,6 +669,17 @@ setInterval(async () => {
639
669
  }, 5000);
640
670
  console.log("mailx client initialized, location:", location.href);
641
671
  updateNewMessageCount();
672
+ // ── Midnight refresh — update date display when day changes ──
673
+ function scheduleMiddnightRefresh() {
674
+ const now = new Date();
675
+ const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
676
+ const ms = midnight.getTime() - now.getTime();
677
+ setTimeout(() => {
678
+ reloadCurrentFolder();
679
+ scheduleMiddnightRefresh();
680
+ }, ms + 1000); // 1s after midnight
681
+ }
682
+ scheduleMiddnightRefresh();
642
683
  // ── Apply theme from settings ──
643
684
  fetch("/api/version").then(r => r.json()).then(d => {
644
685
  if (d.theme === "dark")
package/client/index.html CHANGED
@@ -13,25 +13,10 @@
13
13
  <body>
14
14
  <header class="toolbar">
15
15
  <div class="toolbar-left">
16
+ <button class="tb-btn" id="btn-menu" title="Folders" hidden>☰</button>
16
17
  <button class="tb-btn" id="btn-compose" title="Compose (Ctrl+N)">
17
18
  <span class="tb-icon">✏</span> Compose
18
19
  </button>
19
- <button class="tb-btn" id="btn-reply" title="Reply (Ctrl+R)" disabled>
20
- <span class="tb-icon">↩</span>
21
- </button>
22
- <button class="tb-btn" id="btn-reply-all" title="Reply All (Ctrl+Shift+R)" disabled>
23
- <span class="tb-icon">↩↩</span>
24
- </button>
25
- <button class="tb-btn" id="btn-forward" title="Forward" disabled>
26
- <span class="tb-icon">→</span>
27
- </button>
28
- <span class="tb-sep"></span>
29
- <button class="tb-btn" id="btn-delete" title="Delete (Del)" disabled>
30
- <span class="tb-icon">🗑</span>
31
- </button>
32
- <button class="tb-btn" id="btn-flag" title="Flag" disabled>
33
- <span class="tb-icon">⚑</span>
34
- </button>
35
20
  </div>
36
21
  <div class="toolbar-center">
37
22
  <div class="tb-menu" id="view-menu">
@@ -88,17 +73,22 @@
88
73
 
89
74
  <section class="message-viewer" id="message-viewer">
90
75
  <div class="mv-header" id="mv-header" hidden>
91
- <div class="mv-header-top">
92
- <div class="mv-header-info">
93
- <div class="mv-from"></div>
94
- <div class="mv-to"></div>
95
- </div>
96
- <div class="mv-header-actions">
97
- <button class="mv-action mv-action-primary" id="mv-edit-draft" hidden>Edit & Send</button>
98
- <a class="mv-unsubscribe" id="mv-unsubscribe" hidden>Unsubscribe</a>
99
- <button class="mv-action" id="mv-view-source" title="View source (.eml)" hidden>Source</button>
100
- <button class="mv-action" id="mv-toggle-details" title="Show/hide extra headers">Details</button>
101
- </div>
76
+ <div class="mv-toolbar">
77
+ <button class="tb-btn" id="btn-back" title="Back to list" hidden>←</button>
78
+ <button class="tb-btn" id="btn-reply" title="Reply (Ctrl+R)">↩</button>
79
+ <button class="tb-btn" id="btn-reply-all" title="Reply All (Ctrl+Shift+R)">↩↩</button>
80
+ <button class="tb-btn" id="btn-forward" title="Forward">→</button>
81
+ <button class="tb-btn" id="btn-delete" title="Delete (Del)">🗑</button>
82
+ <button class="tb-btn" id="btn-flag" title="Flag">⚑</button>
83
+ <span style="flex:1"></span>
84
+ <button class="mv-action mv-action-primary" id="mv-edit-draft" hidden>Edit & Send</button>
85
+ <a class="mv-unsubscribe" id="mv-unsubscribe" hidden>Unsubscribe</a>
86
+ <button class="mv-action" id="mv-view-source" title="View source (.eml)" hidden>Source</button>
87
+ <button class="mv-action" id="mv-toggle-details" title="Show/hide extra headers">Details</button>
88
+ </div>
89
+ <div class="mv-header-info">
90
+ <div class="mv-from"></div>
91
+ <div class="mv-to"></div>
102
92
  </div>
103
93
  <div class="mv-subject"></div>
104
94
  <div class="mv-date"></div>
@@ -374,8 +374,15 @@
374
374
  font-size: var(--font-size-sm);
375
375
  line-height: 1.5;
376
376
 
377
- .mv-header-top { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--gap-sm); }
378
- .mv-header-info { flex: 1; min-width: 0; }
377
+ .mv-toolbar {
378
+ display: flex;
379
+ align-items: center;
380
+ gap: var(--gap-xs);
381
+ padding-bottom: var(--gap-xs);
382
+ border-bottom: 1px solid var(--color-border);
383
+ margin-bottom: var(--gap-xs);
384
+ }
385
+ .mv-header-info { }
379
386
  .mv-header-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex-shrink: 0; }
380
387
  .mv-from { font-weight: 600; }
381
388
  .mv-to { color: var(--color-text-muted); }
@@ -55,8 +55,8 @@ body {
55
55
  background: var(--color-accent);
56
56
  }
57
57
 
58
- /* Responsive: narrow viewport stacks everything */
59
- @media (max-width: 800px) {
58
+ /* Responsive: narrow viewport single panel navigation */
59
+ @media (max-width: 768px) {
60
60
  body {
61
61
  grid-template-columns: 1fr;
62
62
  grid-template-areas:
@@ -64,12 +64,64 @@ body {
64
64
  "main"
65
65
  "status";
66
66
  }
67
- .folder-panel { display: none; }
67
+
68
+ /* Folder panel: overlay slide-in from left */
69
+ .folder-panel {
70
+ position: fixed;
71
+ left: -280px;
72
+ top: var(--toolbar-height);
73
+ bottom: var(--statusbar-height);
74
+ width: 280px;
75
+ z-index: 50;
76
+ transition: left 0.2s ease;
77
+ background: var(--color-bg);
78
+ border-right: 1px solid var(--color-border);
79
+ box-shadow: 2px 0 8px rgba(0,0,0,0.3);
80
+ }
81
+ .folder-panel.open { left: 0; }
82
+
83
+ /* Main area: single column */
68
84
  .main-area {
69
85
  grid-template-columns: 1fr;
86
+ grid-template-rows: 1fr;
70
87
  }
71
88
  .splitter { display: none; }
89
+
90
+ /* Show one panel at a time */
72
91
  .message-viewer { display: none; }
73
- .message-viewer.active { display: block; }
74
- .message-list.hidden { display: none; }
92
+ .message-viewer.narrow-active {
93
+ display: flex;
94
+ position: absolute;
95
+ inset: 0;
96
+ z-index: 10;
97
+ background: var(--color-bg);
98
+ }
99
+ .message-list.narrow-hidden { display: none; }
100
+
101
+ /* Show hamburger and back buttons */
102
+ #btn-menu { display: inline-flex !important; }
103
+ #btn-back { display: inline-flex !important; }
104
+
105
+ /* Message list: full width, two-line rows */
106
+ .message-list {
107
+ grid-template-columns: 1.2em 1fr auto;
108
+ }
109
+ .message-list .ml-row {
110
+ grid-template-columns: subgrid;
111
+ grid-template-rows: auto auto;
112
+ }
113
+ .message-list .ml-flag { grid-row: 1 / 3; align-self: center; }
114
+ .message-list .ml-from { grid-column: 2; }
115
+ .message-list .ml-date { grid-column: 3; grid-row: 1; }
116
+ .message-list .ml-subject {
117
+ grid-column: 2 / 4;
118
+ grid-row: 2;
119
+ font-size: var(--font-size-sm);
120
+ color: var(--color-text-muted);
121
+ }
122
+ }
123
+
124
+ /* Hide hamburger and back on wide screens */
125
+ @media (min-width: 769px) {
126
+ #btn-menu, #btn-back { display: none !important; }
75
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.42",
3
+ "version": "1.0.44",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -20,7 +20,7 @@
20
20
  "postinstall": "node launcher/builder/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow": "^1.0.22",
23
+ "@bobfrankston/iflow": "^1.0.24",
24
24
  "@bobfrankston/miscinfo": "^1.0.6",
25
25
  "@bobfrankston/oauthsupport": "^1.0.11",
26
26
  "@bobfrankston/rust-builder": "^0.1.2",
@@ -9,7 +9,7 @@ import * as fs from "node:fs";
9
9
  import { MailxDB } from "@bobfrankston/mailx-store";
10
10
  import { ImapManager } from "@bobfrankston/mailx-imap";
11
11
  import { createApiRouter } from "@bobfrankston/mailx-api";
12
- import { loadSettings, getConfigDir, initLocalConfig } from "@bobfrankston/mailx-settings";
12
+ import { loadSettings, getConfigDir, getSharedDir, initLocalConfig } from "@bobfrankston/mailx-settings";
13
13
  import { ports } from "@bobfrankston/miscinfo";
14
14
  import { createServer } from "node:http";
15
15
  const PORT = ports.mailx;
@@ -81,7 +81,12 @@ app.use("/node_modules", express.static(path.join(rootDir, "node_modules"), { et
81
81
  // Mount API
82
82
  const apiRouter = createApiRouter(db, imapManager);
83
83
  app.use("/api", apiRouter);
84
- app.get("/api/version", (req, res) => res.json({ server: SERVER_VERSION, client: CLIENT_VERSION, theme: settings.ui?.theme || "system" }));
84
+ app.get("/api/version", (req, res) => {
85
+ const sharedDir = getSharedDir();
86
+ const localDir = getConfigDir();
87
+ const drive = sharedDir === localDir ? "local" : sharedDir;
88
+ res.json({ server: SERVER_VERSION, client: CLIENT_VERSION, theme: settings.ui?.theme || "system", drive });
89
+ });
85
90
  app.get("/status", (req, res) => {
86
91
  const accounts = db.getAccounts();
87
92
  const pendingSync = db.getTotalPendingSyncCount();
@@ -91,18 +91,28 @@ function resolveProvider(cfg) {
91
91
  }
92
92
  /** Pending cloud config for API fallback (set when mount not found) */
93
93
  let pendingCloudConfig = null;
94
+ function resolveSharedEntry(entry) {
95
+ if (typeof entry === "string") {
96
+ const p = resolvePath(entry);
97
+ return fs.existsSync(p) ? p : undefined;
98
+ }
99
+ return resolveProvider(entry);
100
+ }
94
101
  function getSharedDir() {
95
102
  const config = readLocalConfig();
96
103
  if (config.sharedDir) {
97
- if (typeof config.sharedDir === "string")
98
- return resolvePath(config.sharedDir);
99
- // Object format: { provider, path }
100
- const resolved = resolveProvider(config.sharedDir);
101
- if (resolved)
102
- return resolved;
103
- // Mount not found — save config for API fallback
104
- pendingCloudConfig = config.sharedDir;
105
- console.log(` ${config.sharedDir.provider} not mounted — will try API`);
104
+ const entries = Array.isArray(config.sharedDir) ? config.sharedDir : [config.sharedDir];
105
+ for (const entry of entries) {
106
+ const resolved = resolveSharedEntry(entry);
107
+ if (resolved)
108
+ return resolved;
109
+ }
110
+ // Nothing mounted — save last provider entry for API fallback
111
+ const lastProvider = [...entries].reverse().find(e => typeof e !== "string");
112
+ if (lastProvider) {
113
+ pendingCloudConfig = lastProvider;
114
+ console.log(` No cloud drive mounted — will try ${lastProvider.provider} API`);
115
+ }
106
116
  }
107
117
  // Legacy: derive from settingsPath
108
118
  if (config.settingsPath)