@bobfrankston/mailx 1.0.394 → 1.0.395

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.
@@ -59,7 +59,8 @@
59
59
  <body>
60
60
  <header class="toolbar">
61
61
  <div class="toolbar-left">
62
- <button class="tb-btn" id="btn-menu" title="Folders" hidden>☰</button>
62
+ <button class="tb-btn" id="btn-menu" title="Menu / rail" hidden>☰</button>
63
+ <button class="tb-btn" id="btn-folder-toggle" title="Show / hide folders" hidden>📁</button>
63
64
  <button class="tb-btn" id="btn-compose" title="Compose (Ctrl+N)">
64
65
  <span class="tb-icon">✏</span> Compose
65
66
  </button>
@@ -68,11 +69,11 @@
68
69
  <div class="tb-menu" id="view-menu">
69
70
  <button class="tb-btn" id="btn-view">View</button>
70
71
  <div class="tb-menu-dropdown" id="view-dropdown" hidden>
71
- <label class="tb-menu-item"><input type="checkbox" id="opt-two-line"> Two-line view</label>
72
- <label class="tb-menu-item"><input type="checkbox" id="opt-preview" checked> Preview pane</label>
73
- <label class="tb-menu-item"><input type="checkbox" id="opt-snippet" checked> Preview snippets</label>
74
- <label class="tb-menu-item"><input type="checkbox" id="opt-flagged"> ★ Flagged only</label>
75
- <label class="tb-menu-item"><input type="checkbox" id="opt-folder-counts"> Folder counts</label>
72
+ <label class="tb-menu-item" title="Stack From + Subject on two lines per row, date to the right — denser on narrow windows"><input type="checkbox" id="opt-two-line"> Two-line view</label>
73
+ <label class="tb-menu-item" title="Show the reading pane below/beside the message list"><input type="checkbox" id="opt-preview" checked> Preview pane</label>
74
+ <label class="tb-menu-item" title="Show a short body-text preview (first ~80 chars) beneath each row's subject"><input type="checkbox" id="opt-snippet" checked> Preview snippets</label>
75
+ <label class="tb-menu-item" title="Show only flagged (★) messages in the list"><input type="checkbox" id="opt-flagged"> ★ Flagged only</label>
76
+ <label class="tb-menu-item" title="Show unread/total counts next to each folder in the tree"><input type="checkbox" id="opt-folder-counts"> Folder counts</label>
76
77
  </div>
77
78
  </div>
78
79
  <div class="tb-menu" id="settings-menu">
@@ -109,6 +110,22 @@
109
110
  <button class="alert-dismiss" id="alert-dismiss" title="Dismiss">&times;</button>
110
111
  </div>
111
112
 
113
+ <aside class="icon-rail" id="icon-rail" aria-label="App rail">
114
+ <div class="rail-top">
115
+ <button class="rail-btn" id="rail-compose" title="Compose" aria-label="Compose">✏</button>
116
+ <button class="rail-btn" id="rail-inbox" title="Inbox" aria-label="Inbox" data-active="true">✉</button>
117
+ <button class="rail-btn" id="rail-unified" title="All Inboxes" aria-label="All Inboxes">⌘</button>
118
+ <button class="rail-btn" id="rail-contacts" title="Contacts" aria-label="Contacts">👤</button>
119
+ <button class="rail-btn" id="rail-calendar" title="Calendar" aria-label="Calendar">📅</button>
120
+ <button class="rail-btn" id="rail-tasks" title="Tasks" aria-label="Tasks">☑</button>
121
+ </div>
122
+ <div class="rail-bottom">
123
+ <button class="rail-btn" id="rail-settings" title="Settings" aria-label="Settings">⚙</button>
124
+ <button class="rail-btn" id="rail-theme" title="Theme" aria-label="Theme">◐</button>
125
+ <button class="rail-btn" id="rail-help" title="Help" aria-label="Help">?</button>
126
+ </div>
127
+ </aside>
128
+
112
129
  <div class="folder-panel">
113
130
  <div class="ft-filter">
114
131
  <input type="text" id="ft-filter-input" placeholder="Find folder..." autocomplete="off">
package/client/app.js CHANGED
@@ -390,9 +390,21 @@ if (messageList) {
390
390
  }
391
391
  }).observe(messageList);
392
392
  }
393
- // ── Narrow screen navigation ──
393
+ // ── Narrow/medium drawer toggles ──
394
+ // Hamburger (☰): rail drawer on narrow; on wider tiers the rail is already
395
+ // visible so this is a no-op visually (the toggle still fires but the rail
396
+ // has no `.open` style to invoke).
397
+ // Folder (📁): folder-panel drawer on any tier where it's positioned as an
398
+ // overlay (medium + narrow).
394
399
  document.getElementById("btn-menu")?.addEventListener("click", () => {
400
+ document.querySelector(".icon-rail")?.classList.toggle("open");
401
+ // Rail drawer and folder drawer are mutually exclusive — opening one
402
+ // closes the other so they don't fight for the left edge.
403
+ document.querySelector(".folder-panel")?.classList.remove("open");
404
+ });
405
+ document.getElementById("btn-folder-toggle")?.addEventListener("click", () => {
395
406
  document.querySelector(".folder-panel")?.classList.toggle("open");
407
+ document.querySelector(".icon-rail")?.classList.remove("open");
396
408
  });
397
409
  const backToList = (e) => {
398
410
  e.preventDefault();
@@ -65,6 +65,34 @@ function clearSelection() {
65
65
  if (body)
66
66
  body.querySelectorAll(".ml-row.selected").forEach(r => r.classList.remove("selected"));
67
67
  }
68
+ /** Exit multi-select mode (entered via touch long-press). Clears selection
69
+ * and the sticky body flag so subsequent taps open messages again. */
70
+ function exitMultiSelect() {
71
+ const body = document.getElementById("ml-body");
72
+ if (!body?.classList.contains("multi-select-on"))
73
+ return;
74
+ body.classList.remove("multi-select-on");
75
+ clearSelection();
76
+ }
77
+ // Escape key + click-outside-list exit multi-select mode. Attached once
78
+ // (idempotent because document only has one listener scope per handler).
79
+ if (!window.__mailxMultiSelectWired) {
80
+ window.__mailxMultiSelectWired = true;
81
+ document.addEventListener("keydown", (e) => {
82
+ if (e.key === "Escape")
83
+ exitMultiSelect();
84
+ });
85
+ document.addEventListener("pointerdown", (e) => {
86
+ const body = document.getElementById("ml-body");
87
+ if (!body?.classList.contains("multi-select-on"))
88
+ return;
89
+ const target = e.target;
90
+ // A tap on a row is handled by the row's own click listener; only
91
+ // exit when the tap is on neutral ground (outside the list entirely).
92
+ if (!target.closest(".ml-row"))
93
+ exitMultiSelect();
94
+ }, true);
95
+ }
68
96
  function selectRange(from, to) {
69
97
  const body = document.getElementById("ml-body");
70
98
  if (!body)
@@ -636,6 +664,15 @@ function appendMessages(body, accountId, items) {
636
664
  touchWasScroll = false;
637
665
  return;
638
666
  }
667
+ // Multi-select mode (entered via long-press on touch): taps toggle
668
+ // rows instead of opening messages. Exit mode when the user taps
669
+ // outside any row or presses Escape (handled at the body level).
670
+ const body = row.parentElement;
671
+ if (body?.classList.contains("multi-select-on")) {
672
+ row.classList.toggle("selected");
673
+ lastClickedRow = row;
674
+ return;
675
+ }
639
676
  if (e.shiftKey && lastClickedRow) {
640
677
  clearSelection();
641
678
  selectRange(lastClickedRow, row);
@@ -700,12 +737,30 @@ function appendMessages(body, accountId, items) {
700
737
  clearTimeout(longPressTimer);
701
738
  longPressTimer = setTimeout(() => {
702
739
  longPressTimer = null;
703
- // Synthesize a contextmenu event so the existing handler below
704
- // owns all the menu logic no per-event duplication.
705
- const ev = new MouseEvent("contextmenu", {
706
- clientX: cx, clientY: cy, bubbles: true, cancelable: true,
707
- });
708
- row.dispatchEvent(ev);
740
+ // Long-press semantics:
741
+ // - If the list is already in multi-select mode, toggle this
742
+ // row's selected state (so the user can extend a selection
743
+ // without needing a second long-press-and-menu dance).
744
+ // - Otherwise enter multi-select mode: mark THIS row selected
745
+ // and add a sticky class on the body so future taps toggle
746
+ // instead of opening messages. Tap elsewhere or press
747
+ // Escape to exit.
748
+ const body = row.parentElement;
749
+ const alreadyMulti = body?.classList.contains("multi-select-on");
750
+ if (alreadyMulti) {
751
+ row.classList.toggle("selected");
752
+ }
753
+ else {
754
+ clearSelection();
755
+ row.classList.add("selected");
756
+ body?.classList.add("multi-select-on");
757
+ }
758
+ lastClickedRow = row;
759
+ // Haptic hint if the platform supports it (Android WebView does).
760
+ try {
761
+ navigator.vibrate?.(20);
762
+ }
763
+ catch { /* */ }
709
764
  }, LONG_PRESS_MS);
710
765
  }, { passive: true });
711
766
  const cancelLongPress = () => {
@@ -800,6 +855,15 @@ function appendMessages(body, accountId, items) {
800
855
  action: () => document.dispatchEvent(new CustomEvent("mailx-delete")),
801
856
  },
802
857
  { label: "", action: () => { }, separator: true },
858
+ {
859
+ label: "⚠ Mark as spam",
860
+ action: () => document.getElementById("btn-spam")?.click(),
861
+ },
862
+ {
863
+ label: "🚫 Report spam",
864
+ action: () => document.getElementById("btn-spam-report")?.click(),
865
+ },
866
+ { label: "", action: () => { }, separator: true },
803
867
  {
804
868
  label: "Copy Message-ID",
805
869
  action: async () => {
package/client/index.html CHANGED
@@ -13,7 +13,8 @@
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
+ <button class="tb-btn" id="btn-menu" title="Menu / rail" hidden>☰</button>
17
+ <button class="tb-btn" id="btn-folder-toggle" title="Show / hide folders" hidden>📁</button>
17
18
  <button class="tb-btn" id="btn-compose" title="Compose (Ctrl+N)">
18
19
  <span class="tb-icon">✏</span><span class="tb-label"> Compose</span>
19
20
  </button>
@@ -22,14 +23,14 @@
22
23
  <div class="tb-menu" id="view-menu">
23
24
  <button class="tb-btn" id="btn-view">View</button>
24
25
  <div class="tb-menu-dropdown" id="view-dropdown" hidden>
25
- <label class="tb-menu-item"><input type="checkbox" id="opt-two-line"> Two-line view</label>
26
- <label class="tb-menu-item"><input type="checkbox" id="opt-preview" checked> Preview pane</label>
27
- <label class="tb-menu-item"><input type="checkbox" id="opt-snippet" checked> Preview snippets</label>
28
- <label class="tb-menu-item"><input type="checkbox" id="opt-threaded"> Group by thread</label>
29
- <label class="tb-menu-item"><input type="checkbox" id="opt-thread-filter"> Only this conversation</label>
30
- <label class="tb-menu-item"><input type="checkbox" id="opt-flagged"> ★ Flagged only</label>
31
- <label class="tb-menu-item"><input type="checkbox" id="opt-folder-counts"> Folder counts</label>
32
- <label class="tb-menu-item"><input type="checkbox" id="opt-calendar-sidebar"> Calendar sidebar</label>
26
+ <label class="tb-menu-item" title="Stack From + Subject on two lines per row, date to the right — denser on narrow windows"><input type="checkbox" id="opt-two-line"> Two-line view</label>
27
+ <label class="tb-menu-item" title="Show the reading pane to the right of the message list"><input type="checkbox" id="opt-preview" checked> Preview pane</label>
28
+ <label class="tb-menu-item" title="Show a short body-text preview (first ~80 chars) beneath each row's subject"><input type="checkbox" id="opt-snippet" checked> Preview snippets</label>
29
+ <label class="tb-menu-item" title="Collapse reply chains to one row showing the newest message; a pill shows thread size"><input type="checkbox" id="opt-threaded"> Group by thread</label>
30
+ <label class="tb-menu-item" title="Filter the list to rows sharing the selected message's thread (set selection first)"><input type="checkbox" id="opt-thread-filter"> Only this conversation</label>
31
+ <label class="tb-menu-item" title="Show only flagged (★) messages in the list"><input type="checkbox" id="opt-flagged"> ★ Flagged only</label>
32
+ <label class="tb-menu-item" title="Show unread/total counts next to each folder in the tree"><input type="checkbox" id="opt-folder-counts"> Folder counts</label>
33
+ <label class="tb-menu-item" title="Show the right-side calendar/tasks sidebar (Thunderbird-Lightning style)"><input type="checkbox" id="opt-calendar-sidebar"> Calendar sidebar</label>
33
34
  </div>
34
35
  </div>
35
36
  <div class="tb-menu" id="settings-menu">
@@ -900,6 +900,23 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
900
900
  list empty when the selection is a singleton thread. */
901
901
  .ml-row.thread-filter-hidden { display: none; }
902
902
 
903
+ /* Multi-select mode (entered by long-press on touch or Ctrl/Shift click on
904
+ desktop). Add a left-edge accent bar so it's visually clear the list is
905
+ in selection mode, not navigation mode. */
906
+ #ml-body.multi-select-on .ml-row::before {
907
+ content: "";
908
+ position: absolute;
909
+ left: 0;
910
+ top: 0;
911
+ bottom: 0;
912
+ width: 3px;
913
+ background: var(--color-brand, oklch(0.65 0.14 250));
914
+ opacity: 0.25;
915
+ }
916
+ #ml-body.multi-select-on .ml-row.selected::before {
917
+ opacity: 1;
918
+ }
919
+
903
920
  /* Info banner inside a modal — used by the Add-contact dialog to surface
904
921
  "already in address book" duplicate notices. Amber tone so it reads as
905
922
  "heads up" not "error". */
@@ -153,10 +153,15 @@ body.calendar-sidebar-on {
153
153
  "status status";
154
154
  }
155
155
 
156
- /* Folder panel: overlay slide-in from left, sitting just to the right of the rail */
156
+ /* Folder panel: FULLY off-screen when closed previously `calc(48px -
157
+ 280px) = -232px` left a 48 px tail showing behind the rail, which on
158
+ Android (where the rail isn't always rendered) leaked a strip of
159
+ folder-row badges at the left edge of the viewport. Now the closed
160
+ panel is flush off-screen at `-280px` and the `.open` slide-in starts
161
+ at the rail's right edge. */
157
162
  .folder-panel {
158
163
  position: fixed;
159
- left: calc(var(--rail-width, 48px) - 280px);
164
+ left: -280px;
160
165
  top: var(--toolbar-height);
161
166
  bottom: var(--statusbar-height);
162
167
  width: 280px;
@@ -168,14 +173,19 @@ body.calendar-sidebar-on {
168
173
  }
169
174
  .folder-panel.open { left: var(--rail-width, 48px); }
170
175
 
171
- /* Show hamburger */
172
- #btn-menu { display: inline-flex !important; }
176
+ /* Medium tier has the rail visible permanently — no hamburger needed.
177
+ Show the folder-toggle (📁) so the user can still reveal the folder
178
+ tree on demand. */
179
+ #btn-menu { display: none !important; }
180
+ #btn-folder-toggle { display: inline-flex !important; }
173
181
  }
174
182
 
175
183
  /* Responsive: narrow OR short viewport — single panel navigation */
176
184
  @media (max-width: 768px), (max-height: 600px) {
177
- /* Hide preview snippet under message subjectsave space */
178
- .ml-preview { display: none; }
185
+ /* Preview snippets remain visible on narrowuser-controlled via the
186
+ View menu's "Preview snippets" checkbox. Previously this rule forced
187
+ them hidden on any narrow viewport, so Android users never saw the
188
+ checkbox take effect. */
179
189
  /* Column headers (From/Date/Subject) take space without being useful on narrow */
180
190
  .ml-header { display: none; }
181
191
  /* Current folder name shown above the list, Dovecot-style */
@@ -208,12 +218,25 @@ body.calendar-sidebar-on {
208
218
  "status";
209
219
  }
210
220
 
211
- /* Rail hidden on narrow its commands fold into the hamburger / toolbar.
212
- Future work: a slide-in rail behind the hamburger so power-users on phone
213
- can still reach calendar/contacts/etc. */
214
- .icon-rail { display: none; }
221
+ /* Rail on narrow: slide-in drawer triggered by the hamburger (☰). The
222
+ rail is the entry point to Inbox / All-Inboxes / Contacts / Calendar /
223
+ Tasks / Settings without this drawer, narrow-tier users couldn't
224
+ reach any of those. */
225
+ .icon-rail {
226
+ display: flex;
227
+ position: fixed;
228
+ left: -64px;
229
+ top: var(--toolbar-height);
230
+ bottom: var(--statusbar-height);
231
+ z-index: 60;
232
+ transition: left 0.2s ease;
233
+ box-shadow: 2px 0 8px rgba(0,0,0,0.3);
234
+ }
235
+ .icon-rail.open { left: 0; }
215
236
 
216
- /* Folder panel: overlay slide-in from left */
237
+ /* Folder panel: slide-in drawer triggered by the folder icon (📁).
238
+ Kept separate from the rail drawer so the user can have folders open
239
+ while the rail is closed, or vice versa — matching the desktop flow. */
217
240
  .folder-panel {
218
241
  position: fixed;
219
242
  left: -280px;
@@ -246,8 +269,10 @@ body.calendar-sidebar-on {
246
269
  }
247
270
  .message-list.narrow-hidden { display: none; }
248
271
 
249
- /* Show hamburger always on narrow */
272
+ /* Hamburger opens the rail drawer; folder-toggle opens the folder drawer.
273
+ Both always visible on narrow so the user can reach either. */
250
274
  #btn-menu { display: inline-flex !important; }
275
+ #btn-folder-toggle { display: inline-flex !important; }
251
276
  /* Back button: only show when viewer is active (message list hidden) */
252
277
  #btn-back { display: none !important; }
253
278
  .message-viewer.narrow-active ~ * #btn-back,
@@ -280,7 +305,8 @@ body.calendar-sidebar-on {
280
305
  }
281
306
  }
282
307
 
283
- /* Hide hamburger and back on wide screens (folder panel always visible) */
308
+ /* Hide hamburger, folder-toggle, and back on wide screens (folder panel
309
+ permanent column, rail permanent column, no drawer toggles needed). */
284
310
  @media (min-width: 1101px) {
285
- #btn-menu, #btn-back { display: none !important; }
311
+ #btn-menu, #btn-folder-toggle, #btn-back { display: none !important; }
286
312
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.394",
3
+ "version": "1.0.395",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",