Compare commits

...

4 commits

Author SHA1 Message Date
Clara K
f68dfe0da2
Fix variable names
thanks, copilot... but no thanks. TF is idx myidx??
2025-12-13 09:17:19 +01:00
ClaraCrazy
b95b9b3db1
[AI] speed up initial loading by using workers 2025-12-13 08:33:25 +01:00
ClaraCrazy
0eda44ea1c
No more using <br> tags for visual space. Lets do things properly 2025-12-13 07:51:43 +01:00
ClaraCrazy
b348fcca93
update milligram CSS 2025-12-13 05:53:30 +01:00
11 changed files with 347 additions and 267 deletions

View file

@ -18,6 +18,8 @@ IMAP_PORT=993 # IMAP port (def
IMAP_TLS=true # Use secure TLS connection (true/false) IMAP_TLS=true # Use secure TLS connection (true/false)
IMAP_AUTH_TIMEOUT=3000 # Authentication timeout in ms IMAP_AUTH_TIMEOUT=3000 # Authentication timeout in ms
IMAP_REFRESH_INTERVAL_SECONDS=60 # Refresh interval for checking new emails IMAP_REFRESH_INTERVAL_SECONDS=60 # Refresh interval for checking new emails
IMAP_FETCH_CHUNK=200 # Number of UIDs per fetch chunk during initial load
IMAP_CONCURRENCY=6 # Number of concurrent fetch workers during initial load
# --- HTTP / WEB CONFIGURATION --- # --- HTTP / WEB CONFIGURATION ---
HTTP_PORT=3000 # Port HTTP_PORT=3000 # Port

3
.gitignore vendored
View file

@ -2,6 +2,5 @@
.idea .idea
.vscode .vscode
.DS_Store .DS_Store
copilot-instructions.md
node_modules node_modules

View file

@ -49,7 +49,9 @@ const config = {
port: Number(process.env.IMAP_PORT), port: Number(process.env.IMAP_PORT),
tls: parseBool(process.env.IMAP_TLS), tls: parseBool(process.env.IMAP_TLS),
authTimeout: Number(process.env.IMAP_AUTH_TIMEOUT), authTimeout: Number(process.env.IMAP_AUTH_TIMEOUT),
refreshIntervalSeconds: Number(process.env.IMAP_REFRESH_INTERVAL_SECONDS) refreshIntervalSeconds: Number(process.env.IMAP_REFRESH_INTERVAL_SECONDS),
fetchChunkSize: Number(process.env.IMAP_FETCH_CHUNK) || 100,
fetchConcurrency: Number(process.env.IMAP_CONCURRENCY) || 6
}, },
http: { http: {

View file

@ -99,6 +99,7 @@ class ImapService extends EventEmitter {
this.loadedUids = new Set() this.loadedUids = new Set()
this.connection = null this.connection = null
this.initialLoadDone = false this.initialLoadDone = false
this.loadingInProgress = false
} }
async connectAndLoadMessages() { async connectAndLoadMessages() {
@ -167,28 +168,58 @@ class ImapService extends EventEmitter {
} }
async _loadMailSummariesAndEmitAsEvents() { async _loadMailSummariesAndEmitAsEvents() {
// UID: Unique id of a message. // Prevent overlapping loads which can inflate counts
if (this.loadingInProgress) {
debug('Load skipped: another load already in progress')
return
}
this.loadingInProgress = true
debug('Starting load of mail summaries')
const uids = await this._getAllUids() const uids = await this._getAllUids()
const newUids = uids.filter(uid => !this.loadedUids.has(uid)) const newUids = uids.filter(uid => !this.loadedUids.has(uid))
debug(`UIDs on server: ${uids.length}, new UIDs to fetch: ${newUids.length}, already loaded: ${this.loadedUids.size}`)
// Optimize by fetching several messages (but not all) with one 'search' call. // Tuneable chunk size & concurrency for faster initial loads
// fetching all at once might be more efficient, but then it takes long until we see any messages const chunkSize = this.config.imap.fetchChunkSize
// in the frontend. With a small chunk size we ensure that we see the newest emails after a few seconds after const concurrency = this.config.imap.fetchConcurrency
// restart.
const uidChunks = _.chunk(newUids, 20)
// Creates an array of functions. We do not start the search now, we just create the function. // Chunk newest-first UIDs to balance speed and first-paint
const fetchFunctions = uidChunks.map(uidChunk => () => const uidChunks = _.chunk(newUids, chunkSize)
this._getMailHeadersAndEmitAsEvents(uidChunk) debug(`Chunk size: ${chunkSize}, concurrency: ${concurrency}, chunks to process: ${uidChunks.length}`)
)
await pSeries(fetchFunctions) // Limited-concurrency worker
const pool = []
let workerId = 0
const runNext = async() => {
if (workerId >= uidChunks.length) return
const chunkId = workerId++
const chunk = uidChunks[chunkId]
try {
debug(`Worker processing chunk ${chunkId + 1}/${uidChunks.length} (size: ${chunk.length})`)
await this._getMailHeadersAndEmitAsEvents(chunk)
debug(`Completed chunk ${chunkId + 1}/${uidChunks.length}; loadedUids size now: ${this.loadedUids.size}`)
} finally {
await runNext()
}
}
// Start workers
const workers = Math.min(concurrency, uidChunks.length)
for (let i = 0; i < workers; i++) {
pool.push(runNext())
}
await Promise.all(pool)
debug(`All chunks processed. Final loadedUids size: ${this.loadedUids.size}`)
// Mark initial load done only after all chunks complete to avoid double-runs
if (!this.initialLoadDone) { if (!this.initialLoadDone) {
this.initialLoadDone = true this.initialLoadDone = true
this.emit(ImapService.EVENT_INITIAL_LOAD_DONE) this.emit(ImapService.EVENT_INITIAL_LOAD_DONE)
debug('Emitted initial load done')
} }
this.loadingInProgress = false
debug('Load finished')
} }
/** /**
@ -341,6 +372,7 @@ class ImapService extends EventEmitter {
async _getMailHeadersAndEmitAsEvents(uids) { async _getMailHeadersAndEmitAsEvents(uids) {
try { try {
const mails = await this._getMailHeaders(uids) const mails = await this._getMailHeaders(uids)
debug(`Fetched headers for ${uids.length} UIDs; server returned ${mails.length} messages`)
mails.forEach(mail => { mails.forEach(mail => {
this.loadedUids.add(mail.attributes.uid) this.loadedUids.add(mail.attributes.uid)
// Some broadcast messages have no TO field. We have to ignore those messages. // Some broadcast messages have no TO field. We have to ignore those messages.

View file

@ -1,40 +1,40 @@
/*! /*!
* Milligram v1.3.0 * Milligram v1.4.1
* https://milligram.github.io * https://milligram.io
* *
* Copyright (c) 2017 CJ Patoilo * Copyright (c) 2020 CJ Patoilo
* Licensed under the MIT license * Licensed under the MIT license
*/ */
*, *,
*:after, *:after,
*:before { *:before {
box-sizing: inherit; box-sizing: inherit;
} }
html { html {
box-sizing: border-box; box-sizing: border-box;
font-size: 62.5%; font-size: 62.5%;
} }
body { body {
color: #606c76; color: #606c76;
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
font-size: 1.6em; font-size: 1.6em;
font-weight: 300; font-weight: 300;
letter-spacing: .01em; letter-spacing: .01em;
line-height: 1.6; line-height: 1.6;
} }
blockquote { blockquote {
border-left: 0.3rem solid #d1d1d1; border-left: 0.3rem solid #d1d1d1;
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
} }
blockquote *:last-child { blockquote *:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.button, .button,
@ -42,25 +42,26 @@ button,
input[type='button'], input[type='button'],
input[type='reset'], input[type='reset'],
input[type='submit'] { input[type='submit'] {
background-color: #9b4dca; background-color: #9b4dca;
border: 0.1rem solid #9b4dca; border: 0.1rem solid #9b4dca;
border-radius: .4rem; border-radius: .4rem;
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 700; font-weight: 700;
height: 3.8rem; height: 3.8rem;
letter-spacing: .1rem; letter-spacing: .1rem;
line-height: 3.8rem; line-height: 3.8rem;
padding: 0 3.0rem; padding: 0 3.0rem;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap; white-space: nowrap;
} }
.button:focus, .button:hover, .button:focus,
.button:hover,
button:focus, button:focus,
button:hover, button:hover,
input[type='button']:focus, input[type='button']:focus,
@ -69,10 +70,10 @@ input[type='reset']:focus,
input[type='reset']:hover, input[type='reset']:hover,
input[type='submit']:focus, input[type='submit']:focus,
input[type='submit']:hover { input[type='submit']:hover {
background-color: #606c76; background-color: #606c76;
border-color: #606c76; border-color: #606c76;
color: #fff; color: #fff;
outline: 0; outline: 0;
} }
.button[disabled], .button[disabled],
@ -80,11 +81,12 @@ button[disabled],
input[type='button'][disabled], input[type='button'][disabled],
input[type='reset'][disabled], input[type='reset'][disabled],
input[type='submit'][disabled] { input[type='submit'][disabled] {
cursor: default; cursor: default;
opacity: .5; opacity: .5;
} }
.button[disabled]:focus, .button[disabled]:hover, .button[disabled]:focus,
.button[disabled]:hover,
button[disabled]:focus, button[disabled]:focus,
button[disabled]:hover, button[disabled]:hover,
input[type='button'][disabled]:focus, input[type='button'][disabled]:focus,
@ -93,8 +95,8 @@ input[type='reset'][disabled]:focus,
input[type='reset'][disabled]:hover, input[type='reset'][disabled]:hover,
input[type='submit'][disabled]:focus, input[type='submit'][disabled]:focus,
input[type='submit'][disabled]:hover { input[type='submit'][disabled]:hover {
background-color: #9b4dca; background-color: #9b4dca;
border-color: #9b4dca; border-color: #9b4dca;
} }
.button.button-outline, .button.button-outline,
@ -102,11 +104,12 @@ button.button-outline,
input[type='button'].button-outline, input[type='button'].button-outline,
input[type='reset'].button-outline, input[type='reset'].button-outline,
input[type='submit'].button-outline { input[type='submit'].button-outline {
background-color: transparent; background-color: transparent;
color: #9b4dca; color: #9b4dca;
} }
.button.button-outline:focus, .button.button-outline:hover, .button.button-outline:focus,
.button.button-outline:hover,
button.button-outline:focus, button.button-outline:focus,
button.button-outline:hover, button.button-outline:hover,
input[type='button'].button-outline:focus, input[type='button'].button-outline:focus,
@ -115,12 +118,13 @@ input[type='reset'].button-outline:focus,
input[type='reset'].button-outline:hover, input[type='reset'].button-outline:hover,
input[type='submit'].button-outline:focus, input[type='submit'].button-outline:focus,
input[type='submit'].button-outline:hover { input[type='submit'].button-outline:hover {
background-color: transparent; background-color: transparent;
border-color: #606c76; border-color: #606c76;
color: #606c76; color: #606c76;
} }
.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover, .button.button-outline[disabled]:focus,
.button.button-outline[disabled]:hover,
button.button-outline[disabled]:focus, button.button-outline[disabled]:focus,
button.button-outline[disabled]:hover, button.button-outline[disabled]:hover,
input[type='button'].button-outline[disabled]:focus, input[type='button'].button-outline[disabled]:focus,
@ -129,8 +133,8 @@ input[type='reset'].button-outline[disabled]:focus,
input[type='reset'].button-outline[disabled]:hover, input[type='reset'].button-outline[disabled]:hover,
input[type='submit'].button-outline[disabled]:focus, input[type='submit'].button-outline[disabled]:focus,
input[type='submit'].button-outline[disabled]:hover { input[type='submit'].button-outline[disabled]:hover {
border-color: inherit; border-color: inherit;
color: #9b4dca; color: #9b4dca;
} }
.button.button-clear, .button.button-clear,
@ -138,12 +142,13 @@ button.button-clear,
input[type='button'].button-clear, input[type='button'].button-clear,
input[type='reset'].button-clear, input[type='reset'].button-clear,
input[type='submit'].button-clear { input[type='submit'].button-clear {
background-color: transparent; background-color: transparent;
border-color: transparent; border-color: transparent;
color: #9b4dca; color: #9b4dca;
} }
.button.button-clear:focus, .button.button-clear:hover, .button.button-clear:focus,
.button.button-clear:hover,
button.button-clear:focus, button.button-clear:focus,
button.button-clear:hover, button.button-clear:hover,
input[type='button'].button-clear:focus, input[type='button'].button-clear:focus,
@ -152,12 +157,13 @@ input[type='reset'].button-clear:focus,
input[type='reset'].button-clear:hover, input[type='reset'].button-clear:hover,
input[type='submit'].button-clear:focus, input[type='submit'].button-clear:focus,
input[type='submit'].button-clear:hover { input[type='submit'].button-clear:hover {
background-color: transparent; background-color: transparent;
border-color: transparent; border-color: transparent;
color: #606c76; color: #606c76;
} }
.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover, .button.button-clear[disabled]:focus,
.button.button-clear[disabled]:hover,
button.button-clear[disabled]:focus, button.button-clear[disabled]:focus,
button.button-clear[disabled]:hover, button.button-clear[disabled]:hover,
input[type='button'].button-clear[disabled]:focus, input[type='button'].button-clear[disabled]:focus,
@ -166,295 +172,324 @@ input[type='reset'].button-clear[disabled]:focus,
input[type='reset'].button-clear[disabled]:hover, input[type='reset'].button-clear[disabled]:hover,
input[type='submit'].button-clear[disabled]:focus, input[type='submit'].button-clear[disabled]:focus,
input[type='submit'].button-clear[disabled]:hover { input[type='submit'].button-clear[disabled]:hover {
color: #9b4dca; color: #9b4dca;
} }
code { code {
background: #f4f5f6; background: #f4f5f6;
border-radius: .4rem; border-radius: .4rem;
font-size: 86%; font-size: 86%;
margin: 0 .2rem; margin: 0 .2rem;
padding: .2rem .5rem; padding: .2rem .5rem;
white-space: nowrap; white-space: nowrap;
} }
pre { pre {
background: #f4f5f6; background: #f4f5f6;
border-left: 0.3rem solid #9b4dca; border-left: 0.3rem solid #9b4dca;
overflow-y: hidden; overflow-y: hidden;
} }
pre > code { pre>code {
border-radius: 0; border-radius: 0;
display: block; display: block;
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
white-space: pre; white-space: pre;
} }
hr { hr {
border: 0; border: 0;
border-top: 0.1rem solid #f4f5f6; border-top: 0.1rem solid #f4f5f6;
margin: 3.0rem 0; margin: 3.0rem 0;
} }
input[type='color'],
input[type='date'],
input[type='datetime'],
input[type='datetime-local'],
input[type='email'], input[type='email'],
input[type='month'],
input[type='number'], input[type='number'],
input[type='password'], input[type='password'],
input[type='search'], input[type='search'],
input[type='tel'], input[type='tel'],
input[type='text'], input[type='text'],
input[type='url'], input[type='url'],
input[type='week'],
input:not([type]),
textarea, textarea,
select { select {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; background-color: transparent;
appearance: none; border: 0.1rem solid #d1d1d1;
background-color: transparent; border-radius: .4rem;
border: 0.1rem solid #d1d1d1; box-shadow: none;
border-radius: .4rem; box-sizing: inherit;
box-shadow: none; height: 3.8rem;
box-sizing: inherit; padding: .6rem 1.0rem .7rem;
height: 3.8rem; width: 100%;
padding: .6rem 1.0rem;
width: 100%;
} }
input[type='color']:focus,
input[type='date']:focus,
input[type='datetime']:focus,
input[type='datetime-local']:focus,
input[type='email']:focus, input[type='email']:focus,
input[type='month']:focus,
input[type='number']:focus, input[type='number']:focus,
input[type='password']:focus, input[type='password']:focus,
input[type='search']:focus, input[type='search']:focus,
input[type='tel']:focus, input[type='tel']:focus,
input[type='text']:focus, input[type='text']:focus,
input[type='url']:focus, input[type='url']:focus,
input[type='week']:focus,
input:not([type]):focus,
textarea:focus, textarea:focus,
select:focus { select:focus {
border-color: #9b4dca; border-color: #9b4dca;
outline: 0; outline: 0;
} }
select { select {
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat; background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" width="30"><path fill="%23d1d1d1" d="M0,0l6,8l6-8"/></svg>') center right no-repeat;
padding-right: 3.0rem; padding-right: 3.0rem;
} }
select:focus { select:focus {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>'); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" width="30"><path fill="%239b4dca" d="M0,0l6,8l6-8"/></svg>');
}
select[multiple] {
background: none;
height: auto;
} }
textarea { textarea {
min-height: 6.5rem; min-height: 6.5rem;
} }
label, label,
legend { legend {
display: block; display: block;
font-size: 1.6rem; font-size: 1.6rem;
font-weight: 700; font-weight: 700;
margin-bottom: .5rem; margin-bottom: .5rem;
} }
fieldset { fieldset {
border-width: 0; border-width: 0;
padding: 0; padding: 0;
} }
input[type='checkbox'], input[type='checkbox'],
input[type='radio'] { input[type='radio'] {
display: inline; display: inline;
} }
.label-inline { .label-inline {
display: inline-block; display: inline-block;
font-weight: normal; font-weight: normal;
margin-left: .5rem; margin-left: .5rem;
} }
.container { .container {
margin: 0 auto; margin: 0 auto;
max-width: 112.0rem; max-width: 112.0rem;
padding: 0 2.0rem; padding: 0 2.0rem;
position: relative; position: relative;
width: 100%; width: 100%;
} }
.row { .row {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0; padding: 0;
width: 100%; width: 100%;
} }
.row.row-no-padding { .row.row-no-padding {
padding: 0; padding: 0;
} }
.row.row-no-padding > .column { .row.row-no-padding>.column {
padding: 0; padding: 0;
} }
.row.row-wrap { .row.row-wrap {
flex-wrap: wrap; flex-wrap: wrap;
} }
.row.row-top { .row.row-top {
align-items: flex-start; align-items: flex-start;
} }
.row.row-bottom { .row.row-bottom {
align-items: flex-end; align-items: flex-end;
} }
.row.row-center { .row.row-center {
align-items: center; align-items: center;
} }
.row.row-stretch { .row.row-stretch {
align-items: stretch; align-items: stretch;
} }
.row.row-baseline { .row.row-baseline {
align-items: baseline; align-items: baseline;
} }
.row .column { .row .column {
display: block; display: block;
flex: 1 1 auto; flex: 1 1 auto;
margin-left: 0; margin-left: 0;
max-width: 100%; max-width: 100%;
width: 100%; width: 100%;
} }
.row .column.column-offset-10 { .row .column.column-offset-10 {
margin-left: 10%; margin-left: 10%;
} }
.row .column.column-offset-20 { .row .column.column-offset-20 {
margin-left: 20%; margin-left: 20%;
} }
.row .column.column-offset-25 { .row .column.column-offset-25 {
margin-left: 25%; margin-left: 25%;
} }
.row .column.column-offset-33, .row .column.column-offset-34 { .row .column.column-offset-33,
margin-left: 33.3333%; .row .column.column-offset-34 {
margin-left: 33.3333%;
}
.row .column.column-offset-40 {
margin-left: 40%;
} }
.row .column.column-offset-50 { .row .column.column-offset-50 {
margin-left: 50%; margin-left: 50%;
} }
.row .column.column-offset-66, .row .column.column-offset-67 { .row .column.column-offset-60 {
margin-left: 66.6666%; margin-left: 60%;
}
.row .column.column-offset-66,
.row .column.column-offset-67 {
margin-left: 66.6666%;
} }
.row .column.column-offset-75 { .row .column.column-offset-75 {
margin-left: 75%; margin-left: 75%;
} }
.row .column.column-offset-80 { .row .column.column-offset-80 {
margin-left: 80%; margin-left: 80%;
} }
.row .column.column-offset-90 { .row .column.column-offset-90 {
margin-left: 90%; margin-left: 90%;
} }
.row .column.column-10 { .row .column.column-10 {
flex: 0 0 10%; flex: 0 0 10%;
max-width: 10%; max-width: 10%;
} }
.row .column.column-20 { .row .column.column-20 {
flex: 0 0 20%; flex: 0 0 20%;
max-width: 20%; max-width: 20%;
} }
.row .column.column-25 { .row .column.column-25 {
flex: 0 0 25%; flex: 0 0 25%;
max-width: 25%; max-width: 25%;
} }
.row .column.column-33, .row .column.column-34 { .row .column.column-33,
flex: 0 0 33.3333%; .row .column.column-34 {
max-width: 33.3333%; flex: 0 0 33.3333%;
max-width: 33.3333%;
} }
.row .column.column-40 { .row .column.column-40 {
flex: 0 0 40%; flex: 0 0 40%;
max-width: 40%; max-width: 40%;
} }
.row .column.column-50 { .row .column.column-50 {
flex: 0 0 50%; flex: 0 0 50%;
max-width: 50%; max-width: 50%;
} }
.row .column.column-60 { .row .column.column-60 {
flex: 0 0 60%; flex: 0 0 60%;
max-width: 60%; max-width: 60%;
} }
.row .column.column-66, .row .column.column-67 { .row .column.column-66,
flex: 0 0 66.6666%; .row .column.column-67 {
max-width: 66.6666%; flex: 0 0 66.6666%;
max-width: 66.6666%;
} }
.row .column.column-75 { .row .column.column-75 {
flex: 0 0 75%; flex: 0 0 75%;
max-width: 75%; max-width: 75%;
} }
.row .column.column-80 { .row .column.column-80 {
flex: 0 0 80%; flex: 0 0 80%;
max-width: 80%; max-width: 80%;
} }
.row .column.column-90 { .row .column.column-90 {
flex: 0 0 90%; flex: 0 0 90%;
max-width: 90%; max-width: 90%;
} }
.row .column .column-top { .row .column .column-top {
align-self: flex-start; align-self: flex-start;
} }
.row .column .column-bottom { .row .column .column-bottom {
align-self: flex-end; align-self: flex-end;
} }
.row .column .column-center { .row .column .column-center {
-ms-grid-row-align: center; align-self: center;
align-self: center;
} }
@media (min-width: 40rem) { @media (min-width: 40rem) {
.row { .row {
flex-direction: row; flex-direction: row;
margin-left: -1.0rem; margin-left: -1.0rem;
width: calc(100% + 2.0rem); width: calc(100% + 2.0rem);
} }
.row .column { .row .column {
margin-bottom: inherit; margin-bottom: inherit;
padding: 0 1.0rem; padding: 0 1.0rem;
} }
} }
a { a {
color: #9b4dca; color: #9b4dca;
text-decoration: none; text-decoration: none;
} }
a:focus, a:hover { a:focus,
color: #606c76; a:hover {
color: #606c76;
} }
dl, dl,
ol, ol,
ul { ul {
list-style: none; list-style: none;
margin-top: 0; margin-top: 0;
padding-left: 0; padding-left: 0;
} }
dl dl, dl dl,
@ -466,16 +501,16 @@ ol ul,
ul dl, ul dl,
ul ol, ul ol,
ul ul { ul ul {
font-size: 90%; font-size: 90%;
margin: 1.5rem 0 1.5rem 3.0rem; margin: 1.5rem 0 1.5rem 3.0rem;
} }
ol { ol {
list-style: decimal inside; list-style: decimal inside;
} }
ul { ul {
list-style: circle inside; list-style: circle inside;
} }
.button, .button,
@ -483,14 +518,14 @@ button,
dd, dd,
dt, dt,
li { li {
margin-bottom: 1.0rem; margin-bottom: 1.0rem;
} }
fieldset, fieldset,
input, input,
select, select,
textarea { textarea {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
blockquote, blockquote,
@ -502,38 +537,47 @@ p,
pre, pre,
table, table,
ul { ul {
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
} }
table { table {
border-spacing: 0; border-spacing: 0;
width: 100%; display: block;
overflow-x: auto;
text-align: left;
width: 100%;
} }
td, td,
th { th {
border-bottom: 0.1rem solid #e1e1e1; border-bottom: 0.1rem solid #e1e1e1;
padding: 1.2rem 1.5rem; padding: 1.2rem 1.5rem;
text-align: left;
} }
td:first-child, td:first-child,
th:first-child { th:first-child {
padding-left: 0; padding-left: 0;
} }
td:last-child, td:last-child,
th:last-child { th:last-child {
padding-right: 0; padding-right: 0;
}
@media (min-width: 40rem) {
table {
display: table;
overflow-x: initial;
}
} }
b, b,
strong { strong {
font-weight: bold; font-weight: bold;
} }
p { p {
margin-top: 0; margin-top: 0;
} }
h1, h1,
@ -542,61 +586,62 @@ h3,
h4, h4,
h5, h5,
h6 { h6 {
font-weight: 300; font-weight: 300;
letter-spacing: -.1rem; letter-spacing: -.1rem;
margin-bottom: 2.0rem; margin-bottom: 2.0rem;
margin-top: 0; margin-top: 0;
} }
h1 { h1 {
font-size: 4.6rem; font-size: 4.6rem;
line-height: 1.2; line-height: 1.2;
} }
h2 { h2 {
font-size: 3.6rem; font-size: 3.6rem;
line-height: 1.25; line-height: 1.25;
} }
h3 { h3 {
font-size: 2.8rem; font-size: 2.8rem;
line-height: 1.3; line-height: 1.3;
} }
h4 { h4 {
font-size: 2.2rem; font-size: 2.2rem;
letter-spacing: -.08rem; letter-spacing: -.08rem;
line-height: 1.35; line-height: 1.35;
} }
h5 { h5 {
font-size: 1.8rem; font-size: 1.8rem;
letter-spacing: -.05rem; letter-spacing: -.05rem;
line-height: 1.5; line-height: 1.5;
} }
h6 { h6 {
font-size: 1.6rem; font-size: 1.6rem;
letter-spacing: 0; letter-spacing: 0;
line-height: 1.4; line-height: 1.4;
} }
img { img {
max-width: 100%; max-width: 100%;
} }
.clearfix:after { .clearfix:after {
clear: both; clear: both;
content: ' '; content: ' ';
display: table; display: table;
} }
.float-left { .float-left {
float: left; float: left;
} }
.float-right { .float-right {
float: right; float: right;
} }
/*# sourceMappingURL=milligram.css.map */ /*# sourceMappingURL=milligram.css.map */

File diff suppressed because one or more lines are too long

View file

@ -69,6 +69,15 @@ text-muted {
margin-top: -2rem margin-top: -2rem
} }
.action-links {
float: right;
text-align: end;
}
.action-links a {
display: block;
}
/* Reset apple form styles */ /* Reset apple form styles */
@ -98,6 +107,10 @@ select:hover {
text-align: center; text-align: center;
} }
#login h4 {
margin-bottom: 6rem;
}
#login fieldset { #login fieldset {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -1,12 +1,9 @@
{% extends 'layout.twig' %} {% extends 'layout.twig' %}
{% block body %} {% block body %}
<div style="float: right; text-align: end;"> <div class="action-links">
<a href="/inbox/{{ address }}"> <a href="/inbox/{{ address }}">← Return to inbox</a>
← Return to inbox</a> <a href="/logout">Logout</a>
<br>
<a href="/logout">
Logout</a>
</div> </div>
<h1>{{message}}</h1> <h1>{{message}}</h1>
<h2>{{error.status}}</h2> <h2>{{error.status}}</h2>

View file

@ -7,11 +7,9 @@
}); });
</script> </script>
<div style="float: right; text-align: end;"> <div class="action-links">
<a href="/inbox/{{ address }}/delete-all"> <a href="/inbox/{{ address }}/delete-all">Wipe Inbox</a>
Wipe Inbox</a><br> <a href="/logout">Logout</a>
<a href="/logout">
Logout</a>
</div> </div>
<h1>{{ address }}</h1> <h1>{{ address }}</h1>
{% for mail in mailSummaries %} {% for mail in mailSummaries %}

View file

@ -2,14 +2,12 @@
{% block body %} {% block body %}
<div style="float: right; text-align: end;"> <div class="action-links">
<a href="/inbox/{{ example }}">Example Inbox</a> <a href="/inbox/{{ example }}">Example Inbox</a>
</div> </div>
<div id="login"> <div id="login">
<h1>Welcome!</h1> <h1>Welcome!</h1>
<h4>Here you can either create a new Inbox, or access your old one</h4> <h4>Here you can either create a new Inbox, or access your old one</h4>
<br>
<br>
{% if userInputError %} {% if userInputError %}
<blockquote class="warning"> <blockquote class="warning">

View file

@ -2,18 +2,11 @@
{% block body %} {% block body %}
<div style="float: right; text-align: end;"> <div class="action-links">
<a href="/inbox/{{ address }}"> <a href="/inbox/{{ address }}">← Return to inbox</a>
← Return to inbox</a> <a href="/inbox/{{ address }}/{{ uid }}/delete">Delete Email</a>
<br> <a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank">View Raw</a>
<a href="/inbox/{{ address }}/{{ uid }}/delete"> <a href="/logout">Logout</a>
Delete Email</a>
<br>
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank">
View Raw</a>
<br>
<a href="/logout">
Logout</a>
</div> </div>
<hr> <hr>
<div class="mail_body" style="padding-left:10%;"> <div class="mail_body" style="padding-left:10%;">