@agenticmail/api 0.7.13 → 0.7.15
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/dist/index.js +18 -0
- package/package.json +1 -1
- package/public/index.html +13 -10
- package/public/js/list-view.js +77 -15
- package/public/styles.css +111 -74
package/dist/index.js
CHANGED
|
@@ -2191,6 +2191,24 @@ function createMailRoutes(accountManager2, config, db, gatewayManager) {
|
|
|
2191
2191
|
next(err);
|
|
2192
2192
|
}
|
|
2193
2193
|
});
|
|
2194
|
+
router.post("/mail/messages/:uid/star", requireAgent, async (req, res, next) => {
|
|
2195
|
+
try {
|
|
2196
|
+
const agent = req.agent;
|
|
2197
|
+
const uid = parseInt(req.params.uid);
|
|
2198
|
+
if (isNaN(uid) || uid < 1) {
|
|
2199
|
+
res.status(400).json({ error: "Invalid UID" });
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
const starred = req.body?.starred !== false;
|
|
2203
|
+
const folder = req.body?.folder || req.query.folder || "INBOX";
|
|
2204
|
+
const password = getAgentPassword(agent);
|
|
2205
|
+
const receiver = await getReceiver(agent.stalwartPrincipal, password, config);
|
|
2206
|
+
await receiver.setStarred(uid, starred, folder);
|
|
2207
|
+
res.json({ ok: true, starred });
|
|
2208
|
+
} catch (err) {
|
|
2209
|
+
next(err);
|
|
2210
|
+
}
|
|
2211
|
+
});
|
|
2194
2212
|
router.post("/mail/messages/:uid/move", requireAgent, async (req, res, next) => {
|
|
2195
2213
|
try {
|
|
2196
2214
|
const agent = req.agent;
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -23,12 +23,14 @@
|
|
|
23
23
|
|
|
24
24
|
<!-- ─── App shell ─────────────────────────────────────────────────── -->
|
|
25
25
|
<div class="app" id="app" style="display:none">
|
|
26
|
-
<!-- Top bar -->
|
|
26
|
+
<!-- Top bar — three-column grid: left chrome, centered search, right buttons. -->
|
|
27
27
|
<header class="topbar">
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
28
|
+
<div class="topbar-left">
|
|
29
|
+
<button class="menu-btn" id="menu-btn" title="Menu" data-icon="menu"></button>
|
|
30
|
+
<div class="brand">
|
|
31
|
+
<img src="/branding/agenticmail-logo.png" alt="AgenticMail" class="brand-logo" />
|
|
32
|
+
<span class="brand-name">AgenticMail</span>
|
|
33
|
+
</div>
|
|
32
34
|
</div>
|
|
33
35
|
<div class="search-container">
|
|
34
36
|
<span class="search-icon" data-icon="search"></span>
|
|
@@ -36,11 +38,12 @@
|
|
|
36
38
|
<span id="search-hint" class="search-hint"></span>
|
|
37
39
|
<button id="search-clear" class="search-clear-btn" title="Clear (Esc)" data-icon="close"></button>
|
|
38
40
|
</div>
|
|
39
|
-
<div class="topbar-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
<div class="topbar-right">
|
|
42
|
+
<button class="icon-btn" id="refresh-btn" title="Refresh (r)" data-icon="refresh"></button>
|
|
43
|
+
<button id="profile-btn" class="profile-trigger" title="Account">
|
|
44
|
+
<span id="profile-avatar"></span>
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
44
47
|
<div id="profile-menu" class="profile-menu">
|
|
45
48
|
<div class="profile-menu-section">Inboxes</div>
|
|
46
49
|
<div id="profile-menu-list"></div>
|
package/public/js/list-view.js
CHANGED
|
@@ -4,7 +4,7 @@ import { state } from './state.js';
|
|
|
4
4
|
import { escapeHtml, toast } from './utils.js';
|
|
5
5
|
import { formatDate } from './time.js';
|
|
6
6
|
import { parseSearch, matchesSearch, highlightTerm } from './search.js';
|
|
7
|
-
import { apiGet } from './api.js';
|
|
7
|
+
import { apiGet, apiPost } from './api.js';
|
|
8
8
|
import { FOLDERS } from './sidebar.js';
|
|
9
9
|
import { icon } from './icons.js';
|
|
10
10
|
|
|
@@ -81,13 +81,22 @@ export async function ensureFolderCache(agent) {
|
|
|
81
81
|
|
|
82
82
|
export async function loadList(agent, folder) {
|
|
83
83
|
const root = document.getElementById('content');
|
|
84
|
+
// Gmail-style toolbar above the list: select-all checkbox,
|
|
85
|
+
// refresh, more-options spacer, count + pagination on the right.
|
|
86
|
+
// Identical layout for every folder so Sent / Drafts / Spam /
|
|
87
|
+
// Trash all share the same UX as Inbox.
|
|
84
88
|
root.innerHTML = `
|
|
85
|
-
<div class="list-
|
|
86
|
-
<
|
|
89
|
+
<div class="list-toolbar">
|
|
90
|
+
<label class="list-select-all" title="Select all">
|
|
91
|
+
<input type="checkbox" id="list-select-all-input" />
|
|
92
|
+
</label>
|
|
93
|
+
<button class="icon-btn list-refresh" title="Refresh" id="list-refresh-btn">${icon('refresh', { size: 18 })}</button>
|
|
94
|
+
<div class="list-toolbar-spacer"></div>
|
|
87
95
|
<span class="count-text" id="list-count"></span>
|
|
88
96
|
</div>
|
|
89
97
|
<div class="list-rows" id="list-rows"><div class="empty">Loading…</div></div>
|
|
90
98
|
`;
|
|
99
|
+
document.getElementById('list-refresh-btn')?.addEventListener('click', () => loadList(agent, folder));
|
|
91
100
|
await ensureFolderCache(agent);
|
|
92
101
|
|
|
93
102
|
// Resolve the real IMAP folder. Starred reuses INBOX + a client-
|
|
@@ -159,6 +168,12 @@ export function renderList() {
|
|
|
159
168
|
return;
|
|
160
169
|
}
|
|
161
170
|
|
|
171
|
+
// Gmail-style single-line row: checkbox · star · sender · subject
|
|
172
|
+
// — preview · date. Subject and preview sit on the same line
|
|
173
|
+
// separated by an em-dash; CSS truncates the joint cell with
|
|
174
|
+
// ellipsis so longer preview lines never wrap. Identical markup
|
|
175
|
+
// for every folder so Sent / Drafts / Spam etc render the same
|
|
176
|
+
// way Inbox does.
|
|
162
177
|
root.innerHTML = filtered.map(m => {
|
|
163
178
|
const unread = !flagsHas(m.flags, '\\Seen');
|
|
164
179
|
const starred = flagsHas(m.flags, '\\Flagged');
|
|
@@ -166,23 +181,19 @@ export function renderList() {
|
|
|
166
181
|
const fromName = m.from?.[0]?.name || fromAddr;
|
|
167
182
|
const subject = m.subject ?? '(no subject)';
|
|
168
183
|
const date = formatDate(m.date);
|
|
169
|
-
const starIcon = icon(starred ? 'starFilled' : 'starOutline', { size:
|
|
170
|
-
// Compact the preview body for the row: collapse whitespace,
|
|
171
|
-
// strip quoted-reply chevrons, cap at a comfortable two-line
|
|
172
|
-
// length. CSS handles the actual line clamp.
|
|
184
|
+
const starIcon = icon(starred ? 'starFilled' : 'starOutline', { size: 16 });
|
|
173
185
|
const cleanPreview = (m.preview ?? '')
|
|
174
186
|
.replace(/^>+ ?/gm, '')
|
|
175
187
|
.replace(/\s+/g, ' ')
|
|
176
|
-
.trim()
|
|
177
|
-
.slice(0, 280);
|
|
188
|
+
.trim();
|
|
178
189
|
return `
|
|
179
190
|
<div class="list-row ${unread ? 'unread' : ''}" data-uid="${m.uid}">
|
|
180
|
-
<
|
|
181
|
-
<span class="
|
|
182
|
-
<span class="from">${highlightTerm(fromName, hlTerm)}</span>
|
|
191
|
+
<label class="row-check" data-action="select"><input type="checkbox" /></label>
|
|
192
|
+
<span class="star ${starred ? 'starred' : ''}" data-action="star" data-uid="${m.uid}">${starIcon}</span>
|
|
193
|
+
<span class="from" title="${escapeHtml(fromAddr)}">${highlightTerm(fromName, hlTerm)}</span>
|
|
183
194
|
<span class="subject-cell">
|
|
184
195
|
<span class="subject">${highlightTerm(subject, hlTerm)}</span>
|
|
185
|
-
|
|
196
|
+
${cleanPreview ? `<span class="preview-sep"> — </span><span class="preview">${highlightTerm(cleanPreview, hlTerm)}</span>` : ''}
|
|
186
197
|
</span>
|
|
187
198
|
<span class="date">${escapeHtml(date)}</span>
|
|
188
199
|
</div>
|
|
@@ -191,9 +202,17 @@ export function renderList() {
|
|
|
191
202
|
|
|
192
203
|
root.querySelectorAll('.list-row').forEach(el => {
|
|
193
204
|
el.addEventListener('click', (e) => {
|
|
194
|
-
|
|
205
|
+
// Star click — toggle via API and optimistically update the
|
|
206
|
+
// local flags so the icon flips without a reload.
|
|
207
|
+
const starEl = e.target.closest('[data-action="star"]');
|
|
208
|
+
if (starEl) {
|
|
209
|
+
e.stopPropagation();
|
|
210
|
+
toggleStar(Number(el.dataset.uid), starEl);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Checkbox click — swallow so we don't navigate.
|
|
214
|
+
if (e.target.closest('[data-action="select"]')) {
|
|
195
215
|
e.stopPropagation();
|
|
196
|
-
toast('Starring not wired through API yet.');
|
|
197
216
|
return;
|
|
198
217
|
}
|
|
199
218
|
const uid = Number(el.dataset.uid);
|
|
@@ -202,6 +221,49 @@ export function renderList() {
|
|
|
202
221
|
});
|
|
203
222
|
}
|
|
204
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Toggle the IMAP \Flagged flag on a message via the API. Updates
|
|
226
|
+
* the in-memory message object on success so renderList reflects
|
|
227
|
+
* the new state without a full reload — and reverts on failure so
|
|
228
|
+
* the icon doesn't drift from server truth.
|
|
229
|
+
*/
|
|
230
|
+
async function toggleStar(uid, starEl) {
|
|
231
|
+
const agent = state.selectedAgent;
|
|
232
|
+
if (!agent) return;
|
|
233
|
+
const msg = state.messages.find(m => m.uid === uid);
|
|
234
|
+
if (!msg) return;
|
|
235
|
+
const wasStarred = flagsHas(msg.flags, '\\Flagged');
|
|
236
|
+
const nextStarred = !wasStarred;
|
|
237
|
+
|
|
238
|
+
// Optimistic UI flip.
|
|
239
|
+
starEl.classList.toggle('starred', nextStarred);
|
|
240
|
+
starEl.innerHTML = icon(nextStarred ? 'starFilled' : 'starOutline', { size: 16 });
|
|
241
|
+
|
|
242
|
+
// Local flags mutation so a re-render keeps the new state.
|
|
243
|
+
const imap = state.folderNames?.[state.selectedFolder] ?? 'INBOX';
|
|
244
|
+
if (Array.isArray(msg.flags)) {
|
|
245
|
+
msg.flags = nextStarred
|
|
246
|
+
? Array.from(new Set([...msg.flags, '\\Flagged']))
|
|
247
|
+
: msg.flags.filter(f => f !== '\\Flagged');
|
|
248
|
+
} else {
|
|
249
|
+
msg.flags = nextStarred ? ['\\Flagged'] : [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
await apiPost(`/mail/messages/${uid}/star`, { starred: nextStarred, folder: imap }, { agentKey: agent.apiKey });
|
|
254
|
+
} catch (err) {
|
|
255
|
+
// Revert on failure.
|
|
256
|
+
starEl.classList.toggle('starred', wasStarred);
|
|
257
|
+
starEl.innerHTML = icon(wasStarred ? 'starFilled' : 'starOutline', { size: 16 });
|
|
258
|
+
if (Array.isArray(msg.flags)) {
|
|
259
|
+
msg.flags = wasStarred
|
|
260
|
+
? Array.from(new Set([...msg.flags, '\\Flagged']))
|
|
261
|
+
: msg.flags.filter(f => f !== '\\Flagged');
|
|
262
|
+
}
|
|
263
|
+
toast(`Star failed: ${err.message}`, true);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
205
267
|
export function clearSearch() {
|
|
206
268
|
const input = document.getElementById('search-input');
|
|
207
269
|
if (input) {
|
package/public/styles.css
CHANGED
|
@@ -65,12 +65,22 @@ a { color: var(--accent-strong); }
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/* ─── Top bar ──────────────────────────────────────────────────────── */
|
|
68
|
+
/* Three-column grid: [menu+brand] · [centered search] · [right buttons].
|
|
69
|
+
A flex topbar with `flex: 1` on both the search and a trailing
|
|
70
|
+
spacer split the remaining space evenly so the search drifted
|
|
71
|
+
left of the visual middle. The grid centers the search column
|
|
72
|
+
in the actual page width regardless of how wide the side groups
|
|
73
|
+
get. */
|
|
68
74
|
.topbar {
|
|
69
|
-
display:
|
|
75
|
+
display: grid;
|
|
76
|
+
grid-template-columns: 1fr auto 1fr;
|
|
77
|
+
align-items: center; gap: 8px;
|
|
70
78
|
padding: 8px 16px;
|
|
71
79
|
background: var(--bg-soft);
|
|
72
80
|
position: relative; z-index: 10;
|
|
73
81
|
}
|
|
82
|
+
.topbar-left { display: flex; align-items: center; gap: 8px; justify-self: start; }
|
|
83
|
+
.topbar-right { display: flex; align-items: center; gap: 4px; justify-self: end; }
|
|
74
84
|
.menu-btn {
|
|
75
85
|
width: 40px; height: 40px; border-radius: 50%;
|
|
76
86
|
display: flex; align-items: center; justify-content: center;
|
|
@@ -101,7 +111,12 @@ a { color: var(--accent-strong); }
|
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
.search-container {
|
|
104
|
-
|
|
114
|
+
/* Lives in the centered grid column of `.topbar`. Width caps at
|
|
115
|
+
720px (Gmail's actual cap), but stretches to fill the column
|
|
116
|
+
when the viewport is narrower. */
|
|
117
|
+
width: 720px;
|
|
118
|
+
max-width: 100%;
|
|
119
|
+
min-width: 280px;
|
|
105
120
|
position: relative;
|
|
106
121
|
}
|
|
107
122
|
.search-input {
|
|
@@ -138,7 +153,6 @@ a { color: var(--accent-strong); }
|
|
|
138
153
|
}
|
|
139
154
|
.search-hint.show { display: block; }
|
|
140
155
|
|
|
141
|
-
.topbar-spacer { flex: 1; }
|
|
142
156
|
.icon-btn {
|
|
143
157
|
width: 40px; height: 40px; border-radius: 50%;
|
|
144
158
|
display: flex; align-items: center; justify-content: center;
|
|
@@ -267,27 +281,43 @@ a { color: var(--accent-strong); }
|
|
|
267
281
|
.main.sidebar-open .sidebar { transform: translateX(0); }
|
|
268
282
|
.main.sidebar-open .sidebar-backdrop { display: block; }
|
|
269
283
|
.content { border-radius: 0; margin: 0; }
|
|
270
|
-
|
|
284
|
+
/* On mobile, drop the centered grid in favour of [chrome | search | account].
|
|
285
|
+
The search expands to fill whatever space is left. */
|
|
286
|
+
.topbar {
|
|
287
|
+
padding: 8px 8px; gap: 4px;
|
|
288
|
+
grid-template-columns: auto 1fr auto;
|
|
289
|
+
}
|
|
271
290
|
.brand { min-width: auto; }
|
|
272
|
-
.brand-name { font-size: 18px; }
|
|
273
|
-
.search-container {
|
|
291
|
+
.brand-name { font-size: 18px; display: none; }
|
|
292
|
+
.search-container { width: auto; min-width: 0; }
|
|
274
293
|
.search-input { height: 40px; font-size: 14px; }
|
|
275
|
-
/*
|
|
276
|
-
|
|
294
|
+
/* On narrow screens, drop the checkbox + From column. Sender goes
|
|
295
|
+
in a small line above subject+preview to mimic Gmail's mobile
|
|
296
|
+
two-row stack. */
|
|
277
297
|
.list-row {
|
|
278
|
-
grid-template-columns:
|
|
279
|
-
height:
|
|
280
|
-
|
|
298
|
+
grid-template-columns: 32px 1fr 64px;
|
|
299
|
+
height: auto;
|
|
300
|
+
min-height: 64px;
|
|
301
|
+
padding: 8px 12px;
|
|
302
|
+
align-items: center;
|
|
303
|
+
}
|
|
304
|
+
.list-row .row-check { display: none; }
|
|
305
|
+
.list-row .from {
|
|
306
|
+
grid-column: 2 / 3;
|
|
307
|
+
font-size: 13px;
|
|
308
|
+
padding-right: 0;
|
|
281
309
|
}
|
|
282
|
-
.list-row .from { display: none; }
|
|
283
310
|
.list-row .subject-cell {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
311
|
+
grid-column: 2 / 3;
|
|
312
|
+
grid-row: 2;
|
|
313
|
+
white-space: normal;
|
|
314
|
+
display: -webkit-box;
|
|
315
|
+
-webkit-line-clamp: 2;
|
|
316
|
+
-webkit-box-orient: vertical;
|
|
317
|
+
overflow: hidden;
|
|
287
318
|
}
|
|
288
|
-
.list-row .
|
|
289
|
-
.list-row .
|
|
290
|
-
.list-row .preview::before { content: ''; }
|
|
319
|
+
.list-row .star { grid-row: 1 / span 2; }
|
|
320
|
+
.list-row .date { grid-row: 1; }
|
|
291
321
|
.message-header { padding: 16px 16px 8px; }
|
|
292
322
|
.message-subject { font-size: 18px; }
|
|
293
323
|
.message-body { padding: 8px 16px 24px; max-width: none; }
|
|
@@ -298,7 +328,6 @@ a { color: var(--accent-strong); }
|
|
|
298
328
|
.compose-modal { width: 100%; max-height: 100vh; border-radius: 0; }
|
|
299
329
|
.compose-body textarea { min-height: 40vh; }
|
|
300
330
|
/* Hide non-essential top-bar buttons on narrow screens. */
|
|
301
|
-
.topbar-spacer { flex: 0; }
|
|
302
331
|
#refresh-btn { display: none; }
|
|
303
332
|
}
|
|
304
333
|
@media (min-width: 801px) {
|
|
@@ -312,8 +341,11 @@ a { color: var(--accent-strong); }
|
|
|
312
341
|
overflow-y: auto;
|
|
313
342
|
}
|
|
314
343
|
.compose-btn {
|
|
344
|
+
/* Gmail's Compose button is 48px tall with 16px corner radius —
|
|
345
|
+
prominent but not pill-shaped. Earlier this was 56px + ~28px
|
|
346
|
+
radius which read as a giant capsule and dominated the sidebar. */
|
|
315
347
|
display: inline-flex; align-items: center; gap: 12px;
|
|
316
|
-
height:
|
|
348
|
+
height: 48px; padding: 0 24px 0 16px;
|
|
317
349
|
margin-bottom: 16px;
|
|
318
350
|
background: var(--pink); color: white;
|
|
319
351
|
border-radius: 16px;
|
|
@@ -364,46 +396,49 @@ a { color: var(--accent-strong); }
|
|
|
364
396
|
display: flex; flex-direction: column;
|
|
365
397
|
}
|
|
366
398
|
|
|
367
|
-
/* List
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
399
|
+
/* List toolbar (sticky, matches Gmail's row of buttons above the list).
|
|
400
|
+
Same markup for every folder so Sent / Drafts / Spam render
|
|
401
|
+
identically to Inbox — no per-folder UX divergence. */
|
|
402
|
+
.list-toolbar {
|
|
403
|
+
display: flex; align-items: center; gap: 4px;
|
|
404
|
+
padding: 4px 16px; height: 48px;
|
|
405
|
+
border-bottom: 1px solid var(--line);
|
|
372
406
|
position: sticky; top: 0; z-index: 5;
|
|
373
407
|
background: var(--bg);
|
|
374
408
|
flex-shrink: 0;
|
|
375
409
|
}
|
|
376
|
-
.list-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
410
|
+
.list-select-all {
|
|
411
|
+
width: 40px; height: 40px;
|
|
412
|
+
display: flex; align-items: center; justify-content: center;
|
|
413
|
+
cursor: pointer; border-radius: 50%;
|
|
380
414
|
}
|
|
381
|
-
.list-
|
|
382
|
-
|
|
383
|
-
|
|
415
|
+
.list-select-all:hover { background: var(--bg-hover); }
|
|
416
|
+
.list-toolbar .list-refresh { width: 40px; height: 40px; }
|
|
417
|
+
.list-toolbar-spacer { flex: 1; }
|
|
418
|
+
.list-toolbar .count-text {
|
|
419
|
+
font-size: 12px; color: var(--muted);
|
|
384
420
|
}
|
|
385
421
|
|
|
386
|
-
/*
|
|
422
|
+
/* Gmail-style compact rows.
|
|
423
|
+
Single line per message; subject + preview share one truncated
|
|
424
|
+
cell so longer previews tail off with ellipsis instead of
|
|
425
|
+
wrapping. Same row shape for every folder. */
|
|
387
426
|
.list-rows {
|
|
388
427
|
flex: 1; overflow-y: auto;
|
|
389
428
|
}
|
|
390
429
|
.list-row {
|
|
391
430
|
display: grid;
|
|
392
|
-
grid-template-columns:
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
align-items: flex-start; gap: 0;
|
|
396
|
-
padding: 10px 16px; min-height: 64px;
|
|
431
|
+
grid-template-columns: 36px 32px 180px 1fr 90px;
|
|
432
|
+
align-items: center; gap: 0;
|
|
433
|
+
padding: 0 16px; height: 36px;
|
|
397
434
|
cursor: pointer;
|
|
398
|
-
border-bottom: 1px solid var(--
|
|
435
|
+
border-bottom: 1px solid var(--line);
|
|
399
436
|
position: relative;
|
|
400
437
|
}
|
|
401
|
-
@media (max-width:
|
|
402
|
-
/* Pull star + dot + date back into the visual midline of the subject. */
|
|
403
|
-
.list-row .star, .list-row .dot, .list-row .date { padding-top: 2px; }
|
|
438
|
+
@media (max-width: 1100px) { .list-row { grid-template-columns: 36px 32px 140px 1fr 80px; } }
|
|
404
439
|
.list-row:hover {
|
|
405
|
-
background: var(--bg
|
|
406
|
-
box-shadow: inset 0 0 0 1px rgba(0,0,0,.
|
|
440
|
+
background: var(--bg);
|
|
441
|
+
box-shadow: inset 1px 0 0 var(--line), inset -1px 0 0 var(--line), 0 1px 3px rgba(0,0,0,.08);
|
|
407
442
|
z-index: 1;
|
|
408
443
|
}
|
|
409
444
|
.list-row.unread { background: var(--bg); }
|
|
@@ -413,52 +448,39 @@ a { color: var(--accent-strong); }
|
|
|
413
448
|
.list-row:not(.unread) .from, .list-row:not(.unread) .subject {
|
|
414
449
|
color: var(--read-text);
|
|
415
450
|
}
|
|
451
|
+
.list-row .row-check {
|
|
452
|
+
width: 36px; height: 36px;
|
|
453
|
+
display: flex; align-items: center; justify-content: center;
|
|
454
|
+
cursor: pointer;
|
|
455
|
+
}
|
|
456
|
+
.list-row .row-check input { cursor: pointer; }
|
|
416
457
|
.list-row .star {
|
|
417
|
-
|
|
418
|
-
width: 24px; height: 24px;
|
|
458
|
+
width: 32px; height: 32px;
|
|
419
459
|
display: flex; align-items: center; justify-content: center;
|
|
420
|
-
border-radius: 50%;
|
|
460
|
+
border-radius: 50%; cursor: pointer;
|
|
461
|
+
color: #dadce0;
|
|
421
462
|
}
|
|
422
|
-
.list-row .star:hover { background: var(--bg-hover); }
|
|
463
|
+
.list-row .star:hover { background: var(--bg-hover); color: var(--muted); }
|
|
423
464
|
.list-row .star.starred { color: #f4b400; }
|
|
424
465
|
.list-row .from {
|
|
425
466
|
font-size: 14px;
|
|
426
467
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
427
|
-
padding-right:
|
|
468
|
+
padding-right: 12px;
|
|
428
469
|
}
|
|
429
470
|
.list-row .subject-cell {
|
|
430
|
-
/* Stacked: subject on top, two-line preview underneath. */
|
|
431
|
-
display: flex; flex-direction: column; gap: 2px;
|
|
432
|
-
overflow: hidden;
|
|
433
|
-
min-width: 0;
|
|
434
|
-
}
|
|
435
|
-
.list-row .subject {
|
|
436
471
|
font-size: 14px;
|
|
437
472
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
473
|
+
min-width: 0;
|
|
474
|
+
display: block; /* one line, ellipsis at the end */
|
|
438
475
|
}
|
|
439
|
-
.list-row .
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
including Firefox/Safari). The break-word stops one long URL
|
|
443
|
-
from blowing out the layout. */
|
|
444
|
-
display: -webkit-box;
|
|
445
|
-
-webkit-line-clamp: 2;
|
|
446
|
-
-webkit-box-orient: vertical;
|
|
447
|
-
overflow: hidden;
|
|
448
|
-
word-break: break-word;
|
|
449
|
-
}
|
|
476
|
+
.list-row .subject { font-weight: inherit; }
|
|
477
|
+
.list-row .preview-sep { color: var(--muted); }
|
|
478
|
+
.list-row .preview { color: var(--muted); font-weight: 400; }
|
|
450
479
|
.list-row .date {
|
|
451
480
|
font-size: 12px; color: var(--muted); font-weight: 500;
|
|
452
481
|
text-align: right; padding-right: 4px;
|
|
453
482
|
}
|
|
454
483
|
.list-row.unread .date { color: var(--unread-bold); font-weight: 700; }
|
|
455
|
-
.list-row .dot {
|
|
456
|
-
width: 8px; height: 8px; border-radius: 50%;
|
|
457
|
-
background: var(--pink);
|
|
458
|
-
display: none;
|
|
459
|
-
margin: 0 auto;
|
|
460
|
-
}
|
|
461
|
-
.list-row.unread .dot { display: block; }
|
|
462
484
|
|
|
463
485
|
mark.search-hl {
|
|
464
486
|
background: #fff475; color: inherit;
|
|
@@ -486,12 +508,22 @@ mark.search-hl {
|
|
|
486
508
|
background: var(--bg);
|
|
487
509
|
position: sticky; top: 0; z-index: 5;
|
|
488
510
|
flex-shrink: 0;
|
|
511
|
+
/* Toolbar spans full width (so the back button sticks to the
|
|
512
|
+
left edge of the content pane) but its action group could be
|
|
513
|
+
centered later if needed. Keeping it full-width matches Gmail. */
|
|
489
514
|
}
|
|
490
515
|
.message-toolbar .icon-btn { width: 36px; height: 36px; font-size: 16px; }
|
|
491
516
|
.message-toolbar .toolbar-spacer { flex: 1; }
|
|
492
517
|
|
|
518
|
+
/* Centered reading column for the open message — Gmail caps body
|
|
519
|
+
content around ~840px so long-line text doesn't sprawl across
|
|
520
|
+
ultrawide displays. Header, body, and attachments all share
|
|
521
|
+
the same centering rule so they line up vertically. */
|
|
493
522
|
.message-header {
|
|
494
523
|
padding: 32px 32px 16px;
|
|
524
|
+
max-width: 840px;
|
|
525
|
+
margin: 0 auto;
|
|
526
|
+
width: 100%;
|
|
495
527
|
}
|
|
496
528
|
.message-subject {
|
|
497
529
|
font: 400 22px/1.3 'Google Sans', sans-serif;
|
|
@@ -520,7 +552,9 @@ mark.search-hl {
|
|
|
520
552
|
font-size: 14px; line-height: 1.65;
|
|
521
553
|
color: var(--ink);
|
|
522
554
|
white-space: pre-wrap; word-wrap: break-word;
|
|
523
|
-
max-width:
|
|
555
|
+
max-width: 840px;
|
|
556
|
+
margin: 0 auto;
|
|
557
|
+
width: 100%;
|
|
524
558
|
}
|
|
525
559
|
.message-body h1, .message-body h2, .message-body h3 {
|
|
526
560
|
color: var(--pink); margin: 1.2em 0 .4em;
|
|
@@ -558,6 +592,9 @@ mark.search-hl {
|
|
|
558
592
|
.message-attachments {
|
|
559
593
|
padding: 12px 32px; border-top: 1px solid var(--line);
|
|
560
594
|
display: flex; flex-wrap: wrap; gap: 8px;
|
|
595
|
+
max-width: 840px;
|
|
596
|
+
margin: 0 auto;
|
|
597
|
+
width: 100%;
|
|
561
598
|
}
|
|
562
599
|
.message-attachment {
|
|
563
600
|
display: inline-flex; align-items: center; gap: 8px;
|