v0.0.3
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-30 00:12:09 +01:00
1 parent a238ff5e23
commit 93762fe9ef
65 files changed
+5420 -2792

No files matched your search

+157 -28
View File
@@ -37,6 +37,7 @@
--field-pad-x: 8px;
--field-pad-y: 5px;
}
html[data-theme="light"] {
color-scheme: light;
--bg: #f2f4f1;
@@ -61,32 +62,137 @@ html[data-theme="light"] {
--shadow: rgba(31, 39, 33, .16);
}
/* User personalization */
:root {
--folder-pane-width: 228px;
--message-pane-width: 420px;
}
html[data-color-preset="blue"] { --accent: #6f96c9; --accent-strong: #86a9d5; --selected: color-mix(in srgb, var(--accent) 20%, var(--panel)); }
html[data-color-preset="slate"] { --accent: #8e9aa8; --accent-strong: #aab4bf; --selected: color-mix(in srgb, var(--accent) 20%, var(--panel)); }
html[data-color-preset="amber"] { --accent: #c69a55; --accent-strong: #d7ad6b; --selected: color-mix(in srgb, var(--accent) 20%, var(--panel)); }
html[data-color-preset="violet"] { --accent: #9b88c9; --accent-strong: #ad9bd5; --selected: color-mix(in srgb, var(--accent) 20%, var(--panel)); }
html[data-color-preset="rose"] { --accent: #c98591; --accent-strong: #d79aa5; --selected: color-mix(in srgb, var(--accent) 20%, var(--panel)); }
html[data-color-preset="custom"] { --bg: var(--user-bg, #121513); --panel: var(--user-panel, #181c19); --panel-2: color-mix(in srgb, var(--panel) 88%, var(--text)); --panel-3: color-mix(in srgb, var(--panel) 80%, var(--text)); --sidebar: color-mix(in srgb, var(--bg) 88%, black); --text: var(--user-text, #edf0ec); --muted: color-mix(in srgb, var(--text) 66%, var(--panel)); --subtle: color-mix(in srgb, var(--text) 52%, var(--panel)); --field: color-mix(in srgb, var(--bg) 82%, var(--panel)); --line: color-mix(in srgb, var(--text) 18%, var(--panel)); --line-strong: color-mix(in srgb, var(--text) 30%, var(--panel)); --hover: color-mix(in srgb, var(--text) 8%, var(--panel)); --accent: var(--user-accent, #6f9c86); --accent-strong: color-mix(in srgb, var(--accent) 82%, white); --selected: color-mix(in srgb, var(--accent) 22%, var(--panel)); }
html[data-font-family="inter"] { --font-sans: Inter, ui-sans-serif, system-ui, sans-serif; }
html[data-font-family="humanist"] { --font-sans: "Segoe UI", Candara, Calibri, ui-sans-serif, system-ui, sans-serif; }
html[data-font-family="mono"] { --font-sans: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; }
html[data-contrast="soft"] { --line: color-mix(in srgb, var(--line) 70%, transparent); --line-strong: color-mix(in srgb, var(--line-strong) 78%, transparent); }
html[data-contrast="high"] { --line: color-mix(in srgb, var(--text) 28%, var(--panel)); --line-strong: color-mix(in srgb, var(--text) 48%, var(--panel)); --muted: color-mix(in srgb, var(--text) 74%, var(--panel)); }
html[data-hide-avatars="true"] .avatar { display: none !important; }
html[data-hide-attachments="true"] .message-attachment, html[data-hide-attachments="true"] .attachment-indicator { display: none !important; }
html[data-hide-flags="true"] .message-flag, html[data-hide-flags="true"] .flag-indicator { display: none !important; }
html[data-reduce-motion="true"] * { scroll-behavior: auto !important; transition: none !important; animation: none !important; }
html[data-color-preset="teal"] { --accent:#4f9d93; --accent-strong:#68b0a5; --selected:color-mix(in srgb,var(--accent) 20%,var(--panel)); }
html[data-color-preset="cyan"] { --accent:#4c9fba; --accent-strong:#68b4cb; --selected:color-mix(in srgb,var(--accent) 20%,var(--panel)); }
html[data-color-preset="indigo"] { --accent:#7184c5; --accent-strong:#8b9bd2; --selected:color-mix(in srgb,var(--accent) 20%,var(--panel)); }
html[data-color-preset="graphite"] { --accent:#9aa19c; --accent-strong:#b0b6b2; --selected:color-mix(in srgb,var(--accent) 18%,var(--panel)); }
html[data-color-preset="orange"] { --accent:#c9824e; --accent-strong:#d69a6d; --selected:color-mix(in srgb,var(--accent) 20%,var(--panel)); }
html[data-color-preset="red"] { --accent:#bd6662; --accent-strong:#cf7e7a; --selected:color-mix(in srgb,var(--accent) 20%,var(--panel)); }
html[data-color-preset="blue"] {
--accent: #6f96c9;
--accent-strong: #86a9d5;
--selected: color-mix(in srgb, var(--accent) 20%, var(--panel));
}
html[data-color-preset="slate"] {
--accent: #8e9aa8;
--accent-strong: #aab4bf;
--selected: color-mix(in srgb, var(--accent) 20%, var(--panel));
}
html[data-color-preset="amber"] {
--accent: #c69a55;
--accent-strong: #d7ad6b;
--selected: color-mix(in srgb, var(--accent) 20%, var(--panel));
}
html[data-color-preset="violet"] {
--accent: #9b88c9;
--accent-strong: #ad9bd5;
--selected: color-mix(in srgb, var(--accent) 20%, var(--panel));
}
html[data-color-preset="rose"] {
--accent: #c98591;
--accent-strong: #d79aa5;
--selected: color-mix(in srgb, var(--accent) 20%, var(--panel));
}
html[data-color-preset="custom"] {
--bg: var(--user-bg, #121513);
--panel: var(--user-panel, #181c19);
--panel-2: color-mix(in srgb, var(--panel) 88%, var(--text));
--panel-3: color-mix(in srgb, var(--panel) 80%, var(--text));
--sidebar: color-mix(in srgb, var(--bg) 88%, black);
--text: var(--user-text, #edf0ec);
--muted: color-mix(in srgb, var(--text) 66%, var(--panel));
--subtle: color-mix(in srgb, var(--text) 52%, var(--panel));
--field: color-mix(in srgb, var(--bg) 82%, var(--panel));
--line: color-mix(in srgb, var(--text) 18%, var(--panel));
--line-strong: color-mix(in srgb, var(--text) 30%, var(--panel));
--hover: color-mix(in srgb, var(--text) 8%, var(--panel));
--accent: var(--user-accent, #6f9c86);
--accent-strong: color-mix(in srgb, var(--accent) 82%, white);
--selected: color-mix(in srgb, var(--accent) 22%, var(--panel));
}
html[data-font-family="inter"] {
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
}
html[data-font-family="humanist"] {
--font-sans: "Segoe UI", Candara, Calibri, ui-sans-serif, system-ui, sans-serif;
}
html[data-font-family="mono"] {
--font-sans: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
}
html[data-contrast="soft"] {
--line: color-mix(in srgb, var(--line) 70%, transparent);
--line-strong: color-mix(in srgb, var(--line-strong) 78%, transparent);
}
html[data-contrast="high"] {
--line: color-mix(in srgb, var(--text) 28%, var(--panel));
--line-strong: color-mix(in srgb, var(--text) 48%, var(--panel));
--muted: color-mix(in srgb, var(--text) 74%, var(--panel));
}
html[data-hide-avatars="true"] .avatar {
display: none !important;
}
html[data-hide-attachments="true"] .message-attachment,
html[data-hide-attachments="true"] .attachment-indicator {
display: none !important;
}
html[data-hide-flags="true"] .message-flag,
html[data-hide-flags="true"] .flag-indicator {
display: none !important;
}
html[data-reduce-motion="true"] * {
scroll-behavior: auto !important;
transition: none !important;
animation: none !important;
}
html[data-color-preset="teal"] {
--accent: #4f9d93;
--accent-strong: #68b0a5;
--selected: color-mix(in srgb, var(--accent) 20%, var(--panel));
}
html[data-color-preset="cyan"] {
--accent: #4c9fba;
--accent-strong: #68b4cb;
--selected: color-mix(in srgb, var(--accent) 20%, var(--panel));
}
html[data-color-preset="indigo"] {
--accent: #7184c5;
--accent-strong: #8b9bd2;
--selected: color-mix(in srgb, var(--accent) 20%, var(--panel));
}
html[data-color-preset="graphite"] {
--accent: #9aa19c;
--accent-strong: #b0b6b2;
--selected: color-mix(in srgb, var(--accent) 18%, var(--panel));
}
html[data-color-preset="orange"] {
--accent: #c9824e;
--accent-strong: #d69a6d;
--selected: color-mix(in srgb, var(--accent) 20%, var(--panel));
}
html[data-color-preset="red"] {
--accent: #bd6662;
--accent-strong: #cf7e7a;
--selected: color-mix(in srgb, var(--accent) 20%, var(--panel));
}
/* Advanced per-user interface controls. */
:root {
@@ -99,11 +205,32 @@ html[data-color-preset="red"] { --accent:#bd6662; --accent-strong:#cf7e7a; --sel
--message-bg: var(--panel);
--reader-bg: var(--panel);
}
html[data-monochrome="true"] { --accent:#929a95; --accent-strong:#aeb4b0; --danger:#9f7775; --warning:#9b8e75; --success:#879482; }
html[data-scrollbar-density="normal"] * { scrollbar-width:auto; }
html[data-scrollbar-density="thin"] * { scrollbar-width:thin; }
html[data-scrollbar-density="hidden"] * { scrollbar-width:none; }
html[data-scrollbar-density="hidden"] *::-webkit-scrollbar { width:0; height:0; }
html[data-monochrome="true"] {
--accent: #929a95;
--accent-strong: #aeb4b0;
--danger: #9f7775;
--warning: #9b8e75;
--success: #879482;
}
html[data-scrollbar-density="normal"] * {
scrollbar-width: auto;
}
html[data-scrollbar-density="thin"] * {
scrollbar-width: thin;
}
html[data-scrollbar-density="hidden"] * {
scrollbar-width: none;
}
html[data-scrollbar-density="hidden"] *::-webkit-scrollbar {
width: 0;
height: 0;
}
html[data-show-borders="false"] .folder-pane,
html[data-show-borders="false"] .message-pane,
html[data-show-borders="false"] .pane-toolbar,
@@ -111,4 +238,6 @@ html[data-show-borders="false"] .reader-toolbar,
html[data-show-borders="false"] .message-header,
html[data-show-borders="false"] .thread-section,
html[data-show-borders="false"] .attachment-list,
html[data-show-borders="false"] .message-row { border-color:transparent !important; }
html[data-show-borders="false"] .message-row {
border-color: transparent !important;
}
+63
View File
@@ -1130,4 +1130,67 @@ html[data-toolbar-density="spacious"] .reader-toolbar {
height: 100%;
max-height: 72dvh;
background: #000;
}
.toolbar-menu {
position: relative;
flex: 0 0 auto;
}
.toolbar-menu>summary {
list-style: none;
user-select: none;
white-space: nowrap;
}
.toolbar-menu>summary::-webkit-details-marker {
display: none;
}
.toolbar-menu>summary::after {
content: "▾";
margin-left: 3px;
color: var(--subtle);
font-size: 9px;
}
.toolbar-menu[open]>summary {
border-color: var(--accent);
background: var(--selected);
}
.toolbar-menu-popover {
position: fixed;
z-index: 160;
top: 0;
left: 0;
min-width: 164px;
padding: 4px;
border: 1px solid var(--line-strong);
border-radius: var(--radius-control);
background: var(--panel);
box-shadow: 0 10px 28px var(--shadow);
}
.toolbar-menu-popover button {
width: 100%;
min-height: 30px;
display: block;
padding: 6px 8px;
border: 0;
border-radius: var(--radius-control);
background: transparent;
color: var(--text);
text-align: left;
white-space: nowrap;
}
.toolbar-menu-popover button:hover,
.toolbar-menu-popover button:focus-visible {
background: var(--hover);
}
#message-transfer-menu .toolbar-menu-popover {
left: auto;
right: 0;
}
+48 -7
View File
@@ -213,7 +213,7 @@
}
.compose-editor:empty::before {
content: "Write a message";
content: attr(data-placeholder);
color: var(--subtle);
}
@@ -295,11 +295,54 @@
}
.compose-mode-control {
margin-left: auto;
flex: 0 0 auto;
justify-self: start;
}
.compose-mode-control select {
min-width: 108px;
width: auto;
min-width: 122px;
}
.compose-footer {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto auto;
align-items: center;
gap: 8px;
}
.compose-submit-actions {
display: inline-flex;
align-items: center;
justify-self: end;
gap: 7px;
}
.compose-footer #compose-status {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.compose-message-size {
justify-self: end;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.compose-message-size.warning {
color: var(--warning);
}
.compose-message-size.over-limit {
color: var(--danger);
font-weight: 650;
}
.compose-dialog.compose-fullscreen .compose-mode-control {
opacity: .9;
}
.compose-editor.plain-mode {
@@ -386,8 +429,6 @@
font-size: 10px;
}
/* Inline image editing */
.compose-editor img[data-rustbox-cid] {
max-width: 100%;
height: auto;
@@ -673,7 +714,6 @@
}
/* Signature/footer editor */
.signature-editor-shell {
overflow: hidden;
border: 1px solid var(--line-strong);
@@ -800,6 +840,7 @@
overflow: auto;
}
}
.compose-response-control {
min-width: 118px;
max-width: 190px;
@@ -809,4 +850,4 @@
min-height: 28px;
padding-top: 2px;
padding-bottom: 2px;
}
}
+40 -7
View File
@@ -1165,7 +1165,6 @@
margin: 6px 0;
}
/* Diagnostics and mailbox behavior */
.diagnostics-grid {
display: grid;
gap: 12px;
@@ -1363,7 +1362,6 @@
min-width: 0;
}
/* Calendar views */
.calendar-toolbar {
display: flex;
flex-wrap: wrap;
@@ -1521,7 +1519,6 @@
}
}
/* 0.0.1 UI reliability pass: viewport-safe dialogs, toasts and calendar layout */
.dialog {
max-width: calc(100vw - 16px);
max-height: calc(100dvh - 16px);
@@ -1539,9 +1536,6 @@
scrollbar-gutter: auto;
}
/* Grid dialogs own their internal scroll regions; simple/legacy dialogs fall
back to scrolling the complete card so actions can never be trapped below
the viewport. */
.compose-card,
.settings-card,
.identity-card,
@@ -1830,6 +1824,41 @@
border-left-color: var(--accent);
}
.toast-progress {
grid-template-columns: 20px minmax(0, 1fr) auto 24px;
}
.toast-progress-content {
min-width: 0;
display: grid;
gap: 6px;
}
.toast-progress-track {
display: block;
width: 100%;
height: 4px;
overflow: hidden;
border-radius: 999px;
background: var(--panel-3);
}
.toast-progress-bar {
display: block;
width: 0;
height: 100%;
border-radius: inherit;
background: var(--accent);
transition: width .12s linear;
}
.toast-progress-value {
min-width: 34px;
color: var(--muted);
font: 10px var(--font-mono);
text-align: right;
}
@media (max-width: 720px) {
.contacts-card,
@@ -2291,7 +2320,6 @@ kbd {
}
}
/* OpenPGP settings */
.pgp-settings-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -2393,4 +2421,9 @@ kbd {
.response-template-meta span {
font-size: 10px;
}
.diagnostic-imap-actions {
justify-content: flex-end;
margin-top: 10px;
}
+626 -3
View File
@@ -1,6 +1,3 @@
/* Fixed interface styles for RustBox 0.0.2.
These styles intentionally ignore RustBox custom palette variables. */
/* ---------- Roundcube Larry ---------- */
html[data-interface-skin="roundcube-larry"] {
--font-sans: "Lucida Grande", "Segoe UI", Arial, Helvetica, sans-serif;
@@ -360,3 +357,629 @@ html[data-interface-skin="high-contrast"] .button.primary {
border-color: #3b4449;
}
}
/* ---------- Modern mail-inspired styles ---------- */
html[data-interface-skin="gmail"],
html[data-interface-skin="proton"],
html[data-interface-skin="zoho"],
html[data-interface-skin="outlook"],
html[data-interface-skin="minimal"] {
--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
html[data-interface-skin="gmail"][data-theme="light"] {
color-scheme: light;
--bg: #f6f8fc;
--panel: #ffffff;
--panel-2: #f8fafd;
--panel-3: #eef3f8;
--sidebar: #eef3f8;
--line: #e2e6ea;
--line-strong: #c7cdd3;
--text: #202124;
--muted: #5f6368;
--subtle: #7a7f85;
--accent: #0b57d0;
--accent-strong: #0842a0;
--accent-ink: #ffffff;
--danger: #b3261e;
--warning: #8a5a00;
--success: #1e8e3e;
--field: #ffffff;
--selected: #d3e3fd;
--hover: #edf2fa;
--shadow: rgba(60, 64, 67, .18);
--folder-bg: #eef3f8;
--message-bg: #ffffff;
--reader-bg: #ffffff;
}
html[data-interface-skin="gmail"][data-theme="dark"] {
color-scheme: dark;
--bg: #1f1f1f;
--panel: #242424;
--panel-2: #2b2b2b;
--panel-3: #333333;
--sidebar: #202124;
--line: #3c4043;
--line-strong: #5f6368;
--text: #e8eaed;
--muted: #bdc1c6;
--subtle: #969ba1;
--accent: #8ab4f8;
--accent-strong: #aecbfa;
--accent-ink: #10223e;
--danger: #f28b82;
--warning: #fdd663;
--success: #81c995;
--field: #202124;
--selected: #2d4668;
--hover: #303134;
--shadow: rgba(0, 0, 0, .38);
--folder-bg: #202124;
--message-bg: #242424;
--reader-bg: #242424;
}
html[data-interface-skin="proton"][data-theme="light"] {
color-scheme: light;
--bg: #f7f5ff;
--panel: #ffffff;
--panel-2: #f7f5ff;
--panel-3: #eeeaff;
--sidebar: #f2efff;
--line: #e0daf7;
--line-strong: #c5bce7;
--text: #28253d;
--muted: #6d6688;
--subtle: #8d86a5;
--accent: #6d4aff;
--accent-strong: #5637d8;
--accent-ink: #ffffff;
--danger: #c43b54;
--warning: #8d6714;
--success: #23845b;
--field: #ffffff;
--selected: #e7e1ff;
--hover: #f0edff;
--shadow: rgba(50, 38, 96, .16);
--folder-bg: #f2efff;
--message-bg: #ffffff;
--reader-bg: #ffffff;
}
html[data-interface-skin="proton"][data-theme="dark"] {
color-scheme: dark;
--bg: #171426;
--panel: #201b34;
--panel-2: #292242;
--panel-3: #332b50;
--sidebar: #1b172d;
--line: #40375e;
--line-strong: #62567f;
--text: #f2efff;
--muted: #beb6d7;
--subtle: #948aae;
--accent: #9b82ff;
--accent-strong: #b5a3ff;
--accent-ink: #171027;
--danger: #f07a91;
--warning: #e1bd62;
--success: #75c7a2;
--field: #1b172d;
--selected: #3d315f;
--hover: #2a2342;
--shadow: rgba(0, 0, 0, .42);
--folder-bg: #1b172d;
--message-bg: #201b34;
--reader-bg: #201b34;
}
html[data-interface-skin="zoho"][data-theme="light"] {
color-scheme: light;
--bg: #f5f7fb;
--panel: #ffffff;
--panel-2: #f8f9fc;
--panel-3: #edf1f6;
--sidebar: #f1f4f8;
--line: #dde3eb;
--line-strong: #bcc7d4;
--text: #202936;
--muted: #647182;
--subtle: #8390a0;
--accent: #2f6fdf;
--accent-strong: #2459b5;
--accent-ink: #ffffff;
--danger: #cf4858;
--warning: #966d11;
--success: #2c8a63;
--field: #ffffff;
--selected: #e1ebff;
--hover: #edf3fd;
--shadow: rgba(31, 45, 66, .15);
--folder-bg: #f1f4f8;
--message-bg: #ffffff;
--reader-bg: #ffffff;
}
html[data-interface-skin="zoho"][data-theme="dark"] {
color-scheme: dark;
--bg: #151a21;
--panel: #1b222c;
--panel-2: #222b36;
--panel-3: #2a3542;
--sidebar: #171e27;
--line: #344251;
--line-strong: #506175;
--text: #eef3f8;
--muted: #abb8c5;
--subtle: #8594a3;
--accent: #73a5ff;
--accent-strong: #91b8ff;
--accent-ink: #0d1c34;
--danger: #ee7b88;
--warning: #dab85d;
--success: #78bf9c;
--field: #171e27;
--selected: #293d5c;
--hover: #25303c;
--shadow: rgba(0, 0, 0, .38);
--folder-bg: #171e27;
--message-bg: #1b222c;
--reader-bg: #1b222c;
}
html[data-interface-skin="outlook"][data-theme="light"] {
color-scheme: light;
--bg: #f5f5f5;
--panel: #ffffff;
--panel-2: #faf9f8;
--panel-3: #f3f2f1;
--sidebar: #f3f2f1;
--line: #e1dfdd;
--line-strong: #c8c6c4;
--text: #242424;
--muted: #616161;
--subtle: #858585;
--accent: #0f6cbd;
--accent-strong: #0b5a9e;
--accent-ink: #ffffff;
--danger: #c50f1f;
--warning: #8a6a00;
--success: #107c10;
--field: #ffffff;
--selected: #deecf9;
--hover: #f3f6fa;
--shadow: rgba(0, 0, 0, .16);
--folder-bg: #f3f2f1;
--message-bg: #ffffff;
--reader-bg: #ffffff;
}
html[data-interface-skin="outlook"][data-theme="dark"] {
color-scheme: dark;
--bg: #1b1a19;
--panel: #252423;
--panel-2: #2d2c2b;
--panel-3: #343332;
--sidebar: #201f1e;
--line: #3b3a39;
--line-strong: #605e5c;
--text: #f3f2f1;
--muted: #c8c6c4;
--subtle: #a19f9d;
--accent: #69afe5;
--accent-strong: #8bc3ea;
--accent-ink: #102334;
--danger: #f1707b;
--warning: #e2c15d;
--success: #7fba7a;
--field: #201f1e;
--selected: #284764;
--hover: #302f2e;
--shadow: rgba(0, 0, 0, .42);
--folder-bg: #201f1e;
--message-bg: #252423;
--reader-bg: #252423;
}
html[data-interface-skin="minimal"][data-theme="light"] {
color-scheme: light;
--bg: #f8f8f7;
--panel: #ffffff;
--panel-2: #fafafa;
--panel-3: #f0f0ef;
--sidebar: #f7f7f6;
--line: #e7e7e5;
--line-strong: #d1d1ce;
--text: #242424;
--muted: #6e6e6b;
--subtle: #92928e;
--accent: #555b62;
--accent-strong: #3d4349;
--accent-ink: #ffffff;
--danger: #b74d4d;
--warning: #836b2c;
--success: #557a59;
--field: #ffffff;
--selected: #ececeb;
--hover: #f2f2f1;
--shadow: rgba(0, 0, 0, .10);
--folder-bg: #f7f7f6;
--message-bg: #ffffff;
--reader-bg: #ffffff;
}
html[data-interface-skin="minimal"][data-theme="dark"] {
color-scheme: dark;
--bg: #161616;
--panel: #1c1c1c;
--panel-2: #222222;
--panel-3: #292929;
--sidebar: #191919;
--line: #303030;
--line-strong: #484848;
--text: #eeeeec;
--muted: #b4b4b0;
--subtle: #8a8a87;
--accent: #b8bdc2;
--accent-strong: #d3d6d9;
--accent-ink: #171717;
--danger: #de7a7a;
--warning: #c5ad6a;
--success: #82a985;
--field: #181818;
--selected: #323232;
--hover: #252525;
--shadow: rgba(0, 0, 0, .35);
--folder-bg: #191919;
--message-bg: #1c1c1c;
--reader-bg: #1c1c1c;
}
html[data-interface-skin="gmail"] .folder-button.active,
html[data-interface-skin="gmail"] .message-row.active,
html[data-interface-skin="gmail"] .message-row.selected,
html[data-interface-skin="gmail"] .settings-tab.active,
html[data-interface-skin="proton"] .folder-button.active,
html[data-interface-skin="proton"] .message-row.active,
html[data-interface-skin="proton"] .message-row.selected,
html[data-interface-skin="proton"] .settings-tab.active,
html[data-interface-skin="zoho"] .folder-button.active,
html[data-interface-skin="zoho"] .message-row.active,
html[data-interface-skin="zoho"] .message-row.selected,
html[data-interface-skin="zoho"] .settings-tab.active,
html[data-interface-skin="outlook"] .folder-button.active,
html[data-interface-skin="outlook"] .message-row.active,
html[data-interface-skin="outlook"] .message-row.selected,
html[data-interface-skin="outlook"] .settings-tab.active,
html[data-interface-skin="minimal"] .folder-button.active,
html[data-interface-skin="minimal"] .message-row.active,
html[data-interface-skin="minimal"] .message-row.selected,
html[data-interface-skin="minimal"] .settings-tab.active {
background: var(--selected);
color: var(--text);
}
html[data-interface-skin="gmail"] .button.primary,
html[data-interface-skin="proton"] .button.primary,
html[data-interface-skin="zoho"] .button.primary,
html[data-interface-skin="outlook"] .button.primary,
html[data-interface-skin="minimal"] .button.primary {
background: var(--accent);
border-color: var(--accent);
color: var(--accent-ink);
}
/* ---------- Additional predefined mail styles ---------- */
html[data-interface-skin="fastmail"],
html[data-interface-skin="apple-mail"],
html[data-interface-skin="yahoo"],
html[data-interface-skin="hey"],
html[data-interface-skin="nord"] {
--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
html[data-interface-skin="fastmail"][data-theme="light"] {
color-scheme: light;
--bg: #f5f8fc;
--panel: #fff;
--panel-2: #f8faff;
--panel-3: #edf3fb;
--sidebar: #f1f5fb;
--line: #dfe6ef;
--line-strong: #bdc9d8;
--text: #172033;
--muted: #607086;
--subtle: #8491a2;
--accent: #2563d9;
--accent-strong: #194eb8;
--accent-ink: #fff;
--danger: #c63f4c;
--warning: #8d6610;
--success: #23805b;
--field: #fff;
--selected: #d9e8ff;
--hover: #edf4ff;
--shadow: rgba(35, 55, 90, .14);
--folder-bg: #f1f5fb;
--message-bg: #fff;
--reader-bg: #fff;
}
html[data-interface-skin="fastmail"][data-theme="dark"] {
color-scheme: dark;
--bg: #151b25;
--panel: #1c2430;
--panel-2: #232d3a;
--panel-3: #2b3746;
--sidebar: #18202b;
--line: #334254;
--line-strong: #52657d;
--text: #eef4fc;
--muted: #b4c0cf;
--subtle: #8998aa;
--accent: #79a8ff;
--accent-strong: #9bbdff;
--accent-ink: #10203b;
--danger: #ef7e89;
--warning: #ddb75c;
--success: #7ac6a0;
--field: #18202b;
--selected: #2b466d;
--hover: #263343;
--shadow: rgba(0, 0, 0, .4);
--folder-bg: #18202b;
--message-bg: #1c2430;
--reader-bg: #1c2430;
}
html[data-interface-skin="apple-mail"][data-theme="light"] {
color-scheme: light;
--bg: #f5f5f7;
--panel: #fff;
--panel-2: #fafafa;
--panel-3: #f0f1f3;
--sidebar: #f3f4f6;
--line: #e2e3e6;
--line-strong: #c7c9ce;
--text: #1d1d1f;
--muted: #66666b;
--subtle: #8d8d93;
--accent: #0a84ff;
--accent-strong: #006bd6;
--accent-ink: #fff;
--danger: #d43b48;
--warning: #946b00;
--success: #24845d;
--field: #fff;
--selected: #dcebff;
--hover: #f0f6ff;
--shadow: rgba(0, 0, 0, .12);
--folder-bg: #f3f4f6;
--message-bg: #fff;
--reader-bg: #fff;
}
html[data-interface-skin="apple-mail"][data-theme="dark"] {
color-scheme: dark;
--bg: #171719;
--panel: #202023;
--panel-2: #27272a;
--panel-3: #303034;
--sidebar: #1b1b1e;
--line: #38383d;
--line-strong: #595960;
--text: #f5f5f7;
--muted: #b7b7bd;
--subtle: #8e8e95;
--accent: #64a8ff;
--accent-strong: #82baff;
--accent-ink: #102237;
--danger: #ff808b;
--warning: #dfbc65;
--success: #78c69f;
--field: #1c1c1f;
--selected: #294867;
--hover: #2a2a2e;
--shadow: rgba(0, 0, 0, .38);
--folder-bg: #1b1b1e;
--message-bg: #202023;
--reader-bg: #202023;
}
html[data-interface-skin="yahoo"][data-theme="light"] {
color-scheme: light;
--bg: #f7f5fb;
--panel: #fff;
--panel-2: #faf8ff;
--panel-3: #f1edfa;
--sidebar: #f4f0fb;
--line: #e6dff1;
--line-strong: #cbbdde;
--text: #24202d;
--muted: #6c6278;
--subtle: #91859e;
--accent: #6001d2;
--accent-strong: #4b00a6;
--accent-ink: #fff;
--danger: #c63f58;
--warning: #8f6810;
--success: #28815e;
--field: #fff;
--selected: #ede3ff;
--hover: #f4edff;
--shadow: rgba(65, 27, 100, .14);
--folder-bg: #f4f0fb;
--message-bg: #fff;
--reader-bg: #fff;
}
html[data-interface-skin="yahoo"][data-theme="dark"] {
color-scheme: dark;
--bg: #1a1420;
--panel: #231a2b;
--panel-2: #2d2137;
--panel-3: #392946;
--sidebar: #1e1726;
--line: #44334f;
--line-strong: #685275;
--text: #f6effb;
--muted: #c8b9d0;
--subtle: #9e8ba9;
--accent: #b686ff;
--accent-strong: #c9a7ff;
--accent-ink: #231038;
--danger: #ef8298;
--warning: #dfbd61;
--success: #7ac5a0;
--field: #1e1726;
--selected: #493166;
--hover: #30243a;
--shadow: rgba(0, 0, 0, .42);
--folder-bg: #1e1726;
--message-bg: #231a2b;
--reader-bg: #231a2b;
}
html[data-interface-skin="hey"][data-theme="light"] {
color-scheme: light;
--bg: #fff8de;
--panel: #fffdf4;
--panel-2: #fff6d0;
--panel-3: #f5e8ae;
--sidebar: #fff3c2;
--line: #eadb99;
--line-strong: #cbb568;
--text: #29251c;
--muted: #756b4d;
--subtle: #9a8e69;
--accent: #e5533d;
--accent-strong: #bd3e2c;
--accent-ink: #fff;
--danger: #c73838;
--warning: #8c6500;
--success: #35795c;
--field: #fffdf4;
--selected: #fff0a8;
--hover: #fff4c5;
--shadow: rgba(90, 70, 15, .14);
--folder-bg: #fff3c2;
--message-bg: #fffdf4;
--reader-bg: #fffdf4;
}
html[data-interface-skin="hey"][data-theme="dark"] {
color-scheme: dark;
--bg: #211f18;
--panel: #29271e;
--panel-2: #333025;
--panel-3: #403a28;
--sidebar: #252219;
--line: #4b4633;
--line-strong: #70684a;
--text: #fff7d8;
--muted: #c9bd91;
--subtle: #9f9470;
--accent: #ff8a6e;
--accent-strong: #ffa18b;
--accent-ink: #34160f;
--danger: #f47c7c;
--warning: #e0bd60;
--success: #81c29e;
--field: #252219;
--selected: #554a25;
--hover: #363223;
--shadow: rgba(0, 0, 0, .38);
--folder-bg: #252219;
--message-bg: #29271e;
--reader-bg: #29271e;
}
html[data-interface-skin="nord"][data-theme="light"] {
color-scheme: light;
--bg: #eceff4;
--panel: #f8fafc;
--panel-2: #eef2f7;
--panel-3: #e5e9f0;
--sidebar: #e5e9f0;
--line: #d2d8e1;
--line-strong: #b4bfcc;
--text: #2e3440;
--muted: #596273;
--subtle: #7a8494;
--accent: #5e81ac;
--accent-strong: #4c6f99;
--accent-ink: #fff;
--danger: #bf616a;
--warning: #a47718;
--success: #4f806c;
--field: #f8fafc;
--selected: #d8dee9;
--hover: #e7ecf2;
--shadow: rgba(46, 52, 64, .15);
--folder-bg: #e5e9f0;
--message-bg: #f8fafc;
--reader-bg: #f8fafc;
}
html[data-interface-skin="nord"][data-theme="dark"] {
color-scheme: dark;
--bg: #2e3440;
--panel: #343b49;
--panel-2: #3b4252;
--panel-3: #434c5e;
--sidebar: #303744;
--line: #4c566a;
--line-strong: #67748c;
--text: #eceff4;
--muted: #c3cad6;
--subtle: #9aa5b6;
--accent: #88c0d0;
--accent-strong: #8fbcbb;
--accent-ink: #223039;
--danger: #bf616a;
--warning: #ebcb8b;
--success: #a3be8c;
--field: #303744;
--selected: #46556c;
--hover: #3b4555;
--shadow: rgba(0, 0, 0, .36);
--folder-bg: #303744;
--message-bg: #343b49;
--reader-bg: #343b49;
}
html[data-interface-skin="fastmail"] .folder-button.active,
html[data-interface-skin="fastmail"] .message-row.active,
html[data-interface-skin="fastmail"] .message-row.selected,
html[data-interface-skin="fastmail"] .settings-tab.active,
html[data-interface-skin="apple-mail"] .folder-button.active,
html[data-interface-skin="apple-mail"] .message-row.active,
html[data-interface-skin="apple-mail"] .message-row.selected,
html[data-interface-skin="apple-mail"] .settings-tab.active,
html[data-interface-skin="yahoo"] .folder-button.active,
html[data-interface-skin="yahoo"] .message-row.active,
html[data-interface-skin="yahoo"] .message-row.selected,
html[data-interface-skin="yahoo"] .settings-tab.active,
html[data-interface-skin="hey"] .folder-button.active,
html[data-interface-skin="hey"] .message-row.active,
html[data-interface-skin="hey"] .message-row.selected,
html[data-interface-skin="hey"] .settings-tab.active,
html[data-interface-skin="nord"] .folder-button.active,
html[data-interface-skin="nord"] .message-row.active,
html[data-interface-skin="nord"] .message-row.selected,
html[data-interface-skin="nord"] .settings-tab.active {
background: var(--selected);
color: var(--text);
}
html[data-interface-skin="fastmail"] .button.primary,
html[data-interface-skin="apple-mail"] .button.primary,
html[data-interface-skin="yahoo"] .button.primary,
html[data-interface-skin="hey"] .button.primary,
html[data-interface-skin="nord"] .button.primary {
background: var(--accent);
border-color: var(--accent);
color: var(--accent-ink);
}
+325
View File
@@ -269,4 +269,329 @@ html[data-preview="bottom"] .reader-pane {
transition: none !important;
animation: none !important;
}
}
/* 0.0.3 mobile reliability pass */
.folder-quota {
display: grid;
gap: 5px;
margin-top: auto;
padding: 9px 0 8px;
border-top: 1px solid var(--line);
}
.folder-quota+.folder-pane-footer {
margin-top: 0;
padding-top: 0;
border-top: 0;
}
.folder-quota-label {
display: flex;
justify-content: space-between;
gap: 8px;
min-width: 0;
color: var(--subtle);
font: 9px var(--font-mono);
}
.folder-quota-label span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.folder-quota-track {
height: 5px;
overflow: hidden;
border-radius: 999px;
background: var(--panel-3);
}
.folder-quota-track span {
display: block;
width: 0;
height: 100%;
border-radius: inherit;
background: var(--accent);
transition: width .16s ease;
}
.folder-quota.quota-warning .folder-quota-track span {
background: var(--warning);
}
.folder-quota.quota-danger .folder-quota-track span {
background: var(--danger);
}
/* The visual Sieve builder must never let checkbox inputs inherit width:100%. */
.sieve-builder .checkbox-row input[type="checkbox"] {
width: 15px;
min-width: 15px;
max-width: 15px;
height: 15px;
flex: 0 0 15px;
padding: 0;
}
.sieve-builder .checkbox-row span {
min-width: 0;
flex: 0 1 auto;
}
.reader-recipients {
cursor: pointer;
border-radius: var(--radius-control);
}
.reader-recipients:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.message-meta.addresses-expanded .sender strong,
.message-meta.addresses-expanded .recipient-line>span {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
}
.message-meta.addresses-expanded {
align-items: start;
}
@media (max-width: 760px) {
.sieve-builder-grid,
.sieve-builder-grid-rule,
.sieve-dynamic-row {
grid-template-columns: minmax(0, 1fr) !important;
}
.sieve-builder .checkbox-row {
align-self: start;
justify-content: flex-start;
min-height: auto;
padding-block: 4px;
}
}
@media (max-width: 560px) {
.dialog {
box-sizing: border-box;
width: calc(100vw - 12px);
max-width: calc(100vw - 12px);
overflow-x: hidden;
}
.dialog>.dialog-card {
box-sizing: border-box;
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
}
.dialog-header,
.dialog-actions,
.settings-panel,
.settings-panel-heading,
.settings-section,
.settings-grid,
.appearance-grid,
.appearance-toggles,
.appearance-presets,
.sieve-layout,
.sieve-editor-wrap,
.headers-content,
.message-source-content {
min-width: 0;
max-width: 100%;
}
.dialog-header {
width: 100%;
}
.dialog-actions {
flex-wrap: wrap;
gap: 6px;
}
.dialog-actions .toolbar-spacer {
display: none;
}
.settings-card {
width: 100%;
height: calc(100dvh - 12px);
}
.settings-layout {
width: 100%;
min-width: 0;
height: 100%;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr);
align-content: start;
overflow: hidden;
}
.settings-nav {
width: 100%;
min-width: 0;
min-height: auto;
max-height: none;
align-items: center;
padding: 6px;
}
.settings-tab {
align-self: center;
min-height: 32px;
padding: 5px 8px;
}
.settings-content {
width: 100%;
min-width: 0;
max-width: 100%;
padding: 12px;
overflow-x: hidden;
overflow-y: auto;
}
.settings-content * {
max-width: 100%;
}
.settings-content .settings-grid,
.settings-content .appearance-grid,
.settings-content .appearance-toggles,
.settings-content .notification-folder-list,
.settings-content .pgp-settings-grid {
grid-template-columns: minmax(0, 1fr) !important;
}
.settings-panel-heading,
.sieve-header {
flex-wrap: wrap;
}
.settings-panel-heading>.button,
.sieve-header>.inline-actions {
max-width: 100%;
}
.headers-card,
.source-card,
.calendar-card,
.contacts-card {
width: 100% !important;
max-width: 100% !important;
}
.headers-content,
.message-source-content {
width: 100%;
overflow-x: auto;
}
.calendar-toolbar {
display: grid !important;
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
gap: 7px;
}
.calendar-toolbar>* {
min-width: 0 !important;
width: 100%;
max-width: 100%;
}
.calendar-toolbar #calendar-range {
grid-column: 1 / -1;
order: -2;
min-height: 24px;
}
.calendar-toolbar #calendar-view {
grid-column: 1 / -1;
}
.calendar-toolbar #calendar-new {
grid-column: 1 / -1;
}
.calendar-toolbar .button,
.calendar-toolbar select {
white-space: normal;
}
.message-meta {
grid-template-columns: 32px minmax(0, 1fr);
}
.message-meta time {
grid-column: 2;
justify-self: start;
}
}
@media (max-width: 560px) {
.reader-toolbar .toolbar-menu>summary span {
display: none;
}
.reader-toolbar .toolbar-menu>summary {
min-width: 30px;
padding-inline: 7px;
}
.reader-toolbar .toolbar-menu-popover {
position: fixed;
top: auto;
right: 8px;
bottom: 8px;
left: 8px;
min-width: 0;
max-width: none;
}
.compose-footer {
grid-template-columns: minmax(0, 1fr) auto;
}
.compose-footer #compose-status {
grid-column: 1;
grid-row: 1;
min-height: 0;
}
.compose-message-size {
grid-column: 2;
grid-row: 1;
justify-self: end;
font-size: 11px;
}
.compose-mode-control {
grid-column: 1;
grid-row: 2;
justify-self: start;
margin-left: 0;
}
.compose-submit-actions {
grid-column: 2;
grid-row: 2;
justify-self: end;
}
.compose-mode-control select {
min-width: 108px;
}
}
+20 -1
View File
@@ -36,5 +36,24 @@
"print_message": "Print message",
"create_filter_sender": "Create filter from sender",
"filter_draft_created": "Filter draft created from message",
"select_all_results": "Select all results"
"select_all_results": "Select all results",
"loading": "Loading…",
"loading_quota": "Loading quota…",
"of": "of",
"used": "used",
"server_limit_unavailable": "server limit unavailable",
"quota_unavailable": "Quota unavailable",
"unavailable": "Unavailable",
"show_full_addresses": "Show full addresses",
"collapse_addresses": "Collapse addresses",
"fixed_interface_style_note": "fixed interface style. Theme still switches light/dark; choose RustBox (custom) to edit colors, spacing and geometry.",
"custom_interface_style_note": "RustBox is fully customizable: colors, spacing, panes, typography and advanced interface controls are available below.",
"reply": "Reply",
"reply_all": "Reply all",
"forward": "Forward",
"headers": "Headers",
"message_details": "Message details",
"move_copy": "Move / copy",
"move_to": "Move to…",
"copy_to": "Copy to…"
}
+594 -1
View File
@@ -36,5 +36,598 @@
"print_message": "Drukuj wiadomość",
"create_filter_sender": "Utwórz filtr z nadawcy",
"filter_draft_created": "Utworzono szkic filtra z wiadomości",
"select_all_results": "Zaznacz wszystkie wyniki"
"select_all_results": "Zaznacz wszystkie wyniki",
"loading": "Ładowanie…",
"loading_quota": "Ładowanie limitu…",
"of": "z",
"used": "wykorzystano",
"server_limit_unavailable": "limit serwera niedostępny",
"quota_unavailable": "Limit niedostępny",
"unavailable": "Niedostępne",
"show_full_addresses": "Pokaż pełne adresy",
"collapse_addresses": "Zwiń adresy",
"_text": {
"Webmail": "Poczta internetowa",
"Email": "E-mail",
"Password": "Hasło",
"Keep me signed in": "Zapamiętaj mnie",
"Sign in": "Zaloguj się",
"Connecting": "Łączenie",
"Folders": "Foldery",
"Compose": "Napisz",
"Labels": "Etykiety",
"IMAP storage": "Pojemność IMAP",
"Loading…": "Ładowanie…",
"New message": "Nowa wiadomość",
"Recover draft": "Odzyskaj szkic",
"From": "Od",
"Account default": "Domyślne dla konta",
"To": "Do",
"Cc": "DW",
"Bcc": "UDW",
"Subject": "Temat",
"Options": "Opcje",
"Priority": "Priorytet",
"Normal": "Normalny",
"High": "Wysoki",
"Low": "Niski",
"Off": "Wyłączone",
"Sign": "Podpisz",
"Encrypt": "Szyfruj",
"Sign + encrypt": "Podpisz i szyfruj",
"Request read receipt": "Żądaj potwierdzenia odczytu",
"OpenPGP disabled": "OpenPGP wyłączone",
"Details": "Szczegóły",
"Inline OpenPGP (text only; PGP/MIME recommended)": "OpenPGP inline (tylko tekst; zalecane PGP/MIME)",
"Attach my public OpenPGP key": "Dołącz mój publiczny klucz OpenPGP",
"Attach public keys of To/Cc recipients": "Dołącz klucze publiczne odbiorców Do/DW",
"Bcc recipient keys are never attached, so hidden recipients are not disclosed.": "Klucze odbiorców UDW nie są dołączane, aby nie ujawniać ukrytych odbiorców.",
"Private-key passphrase": "Hasło klucza prywatnego",
"Check recipient keys": "Sprawdź klucze odbiorców",
"Find missing keys": "Znajdź brakujące klucze",
"Font": "Czcionka",
"Text size": "Rozmiar tekstu",
"Size": "Rozmiar",
"Small": "Mały",
"Large": "Duży",
"Larger": "Większy",
"Text color": "Kolor tekstu",
"Saved response": "Zapisana odpowiedź",
"Response…": "Odpowiedź…",
"Compose format": "Format wiadomości",
"Plain text": "Zwykły tekst",
"Uploading…": "Wysyłanie…",
"Cancel": "Anuluj",
"Send": "Wyślij",
"Create folder": "Utwórz folder",
"Create": "Utwórz",
"Current folder": "Bieżący folder",
"Subscribe / Unsubscribe": "Subskrybuj / anuluj subskrypcję",
"Rename": "Zmień nazwę",
"Delete": "Usuń",
"Settings": "Ustawienia",
"Appearance": "Wygląd",
"Mailbox": "Skrzynka",
"Composing": "Redagowanie",
"Identities & signatures": "Tożsamości i podpisy",
"Notifications": "Powiadomienia",
"Filters": "Filtry",
"Diagnostics": "Diagnostyka",
"Account": "Konto",
"About": "Informacje",
"Personalize RustBox without changing mail behavior.": "Dostosuj wygląd RustBox bez zmiany działania poczty.",
"Reset appearance": "Przywróć wygląd",
"Language": "Język",
"English": "Angielski",
"Theme": "Motyw",
"Dark": "Ciemny",
"Light": "Jasny",
"System": "Systemowy",
"Interface style": "Styl interfejsu",
"RustBox (custom)": "RustBox (niestandardowy)",
"Global default": "Domyślny globalny",
"Gmail inspired": "Inspiracja Gmail",
"Proton inspired": "Inspiracja Proton",
"Zoho inspired": "Inspiracja Zoho",
"Outlook inspired": "Inspiracja Outlook",
"High contrast": "Wysoki kontrast",
"RustBox is fully customizable: colors, spacing, panes and typography.": "RustBox można w pełni dostosować: kolory, odstępy, panele i typografię.",
"Color preset": "Paleta kolorów",
"Custom": "Niestandardowy",
"Accent color": "Kolor akcentu",
"Custom background": "Niestandardowe tło",
"Custom panel": "Niestandardowy panel",
"Custom text": "Niestandardowy tekst",
"Density": "Gęstość",
"Compact": "Kompaktowa",
"Comfortable": "Wygodna",
"Preview pane": "Panel podglądu",
"Right": "Prawo",
"Bottom": "Dół",
"UI size": "Rozmiar interfejsu",
"Font family": "Krój pisma",
"System UI": "Systemowy",
"Humanist": "Humanistyczny",
"Monospace": "Monospace",
"Font size": "Rozmiar tekstu",
"Message row height": "Wysokość wiersza wiadomości",
"Folder pane width": "Szerokość panelu folderów",
"Message pane width": "Szerokość listy wiadomości",
"Corner radius": "Zaokrąglenie narożników",
"Contrast": "Kontrast",
"Soft": "Łagodny",
"Date format": "Format daty",
"Locale": "Lokalny",
"Time format": "Format godziny",
"24-hour": "24-godzinny",
"12-hour": "12-godzinny",
"Show avatars": "Pokazuj awatary",
"Show attachment icons": "Pokazuj ikony załączników",
"Show flags": "Pokazuj flagi",
"Reduce motion": "Ogranicz animacje",
"Advanced interface styling": "Zaawansowany wygląd interfejsu",
"Icon size": "Rozmiar ikon",
"Toolbar density": "Gęstość paska narzędzi",
"Spacious": "Przestronna",
"Sidebar item height": "Wysokość elementu paska bocznego",
"Reader max width": "Maks. szerokość czytnika",
"Fluid": "Płynna",
"Composer width": "Szerokość edytora",
"Selection color": "Kolor zaznaczenia",
"Unread weight": "Grubość nieprzeczytanych",
"Unread marker": "Znacznik nieprzeczytanych",
"Dot": "Kropka",
"Outline": "Obrys",
"Solid": "Pełny",
"Scrollbar": "Pasek przewijania",
"Thin": "Cienki",
"Hidden": "Ukryty",
"Compact message headers": "Kompaktowe nagłówki wiadomości",
"Show panel borders": "Pokazuj obramowania paneli",
"Monochrome interface": "Monochromatyczny interfejs",
"Folder background override": "Nadpisanie tła folderów",
"Message-list background override": "Nadpisanie tła listy wiadomości",
"Reader background override": "Nadpisanie tła czytnika",
"Control message-list and reader behavior for this account.": "Ustaw zachowanie listy wiadomości i czytnika dla tego konta.",
"Messages per page": "Wiadomości na stronę",
"Mark as read": "Oznacz jako przeczytane",
"Immediately": "Natychmiast",
"After 0.75 s": "Po 0,75 s",
"After 1.5 s": "Po 1,5 s",
"After 3 s": "Po 3 s",
"After 5 s": "Po 5 s",
"Remote images": "Zdalne obrazy",
"Never": "Nigdy",
"Contacts only": "Tylko kontakty",
"Ask for each message": "Pytaj dla każdej wiadomości",
"Always load": "Zawsze wczytuj",
"Message body": "Treść wiadomości",
"Auto": "Automatycznie",
"Prefer HTML": "Preferuj HTML",
"Threads": "Wątki",
"Threaded": "Wątki",
"Flat": "Płasko",
"Default sort": "Domyślne sortowanie",
"Newest first": "Najnowsze najpierw",
"Oldest first": "Najstarsze najpierw",
"Sender A–Z": "Nadawca A–Z",
"Sender Z–A": "Nadawca Z–A",
"Subject A–Z": "Temat A–Z",
"Subject Z–A": "Temat Z–A",
"Smallest first": "Najmniejsze najpierw",
"Largest first": "Największe najpierw",
"Collapse quoted text where possible": "Zwijaj cytowany tekst, gdy to możliwe",
"Show message snippets in the list": "Pokazuj fragmenty wiadomości na liście",
"Keep flagged messages on top by default": "Domyślnie trzymaj oznaczone wiadomości na górze",
"Synchronize RustBox labels as IMAP keywords": "Synchronizuj etykiety RustBox jako słowa kluczowe IMAP",
"Keyword synchronization is used only in folders whose IMAP server advertises persistent custom flags. Local labels remain available everywhere.": "Synchronizacja słów kluczowych działa tylko w folderach, w których serwer IMAP obsługuje trwałe własne flagi. Lokalne etykiety pozostają dostępne wszędzie.",
"Save mailbox settings": "Zapisz ustawienia skrzynki",
"Reconcile IMAP labels now": "Synchronizuj etykiety IMAP teraz",
"Defaults for new messages and draft behavior.": "Domyślne ustawienia nowych wiadomości i szkiców.",
"Default format": "Domyślny format",
"Autosave interval": "Interwał autozapisu",
"1 second": "1 sekunda",
"3 seconds": "3 sekundy",
"5 seconds": "5 sekund",
"10 seconds": "10 sekund",
"20 seconds": "20 sekund",
"60 seconds": "60 sekund",
"Default priority": "Domyślny priorytet",
"Default message font": "Domyślna czcionka wiadomości",
"System sans": "Systemowy sans",
"Humanist sans": "Humanistyczny sans",
"Serif": "Szeryfowa",
"Reply position": "Pozycja odpowiedzi",
"Write above quote": "Pisz nad cytatem",
"Write below quote": "Pisz pod cytatem",
"Quote style": "Styl cytatu",
"Standard attribution": "Standardowe przypisanie",
"Compact attribution": "Kompaktowe przypisanie",
"Do not include quote": "Nie dołączaj cytatu",
"Forward button": "Przycisk przekazywania",
"Forward inline": "Przekazuj w treści",
"Forward as .eml attachment": "Przekazuj jako załącznik .eml",
"Request read receipt by default": "Domyślnie żądaj potwierdzenia odczytu",
"Warn when message text suggests a missing attachment": "Ostrzegaj, gdy treść sugeruje brak załącznika",
"Use browser spellcheck while composing": "Używaj sprawdzania pisowni przeglądarki",
"Markdown is stored as source text and sent as a safe rendered HTML alternative with a plain-text fallback.": "Markdown jest przechowywany jako tekst źródłowy i wysyłany jako bezpiecznie wyrenderowany HTML z wersją tekstową.",
"Saved responses": "Zapisane odpowiedzi",
"Reusable text or HTML responses, compatible with the Roundcube canned-response workflow.": "Wielokrotnego użytku odpowiedzi tekstowe lub HTML, zgodne z przepływem gotowych odpowiedzi Roundcube.",
"New response": "Nowa odpowiedź",
"No saved responses.": "Brak zapisanych odpowiedzi.",
"Choose special IMAP folders used by RustBox. Values are stored per account.": "Wybierz specjalne foldery IMAP używane przez RustBox. Wartości są zapisywane osobno dla konta.",
"Auto-detect": "Wykryj automatycznie",
"Sent": "Wysłane",
"Drafts": "Szkice",
"Trash": "Kosz",
"Default spam destination": "Domyślny folder spamu",
"Shared by all accounts in this browser when that folder exists.": "Wspólne dla wszystkich kont w tej przeglądarce, jeśli folder istnieje.",
"Spam destination override (this account)": "Nadpisanie folderu spamu (to konto)",
"Leave automatic to use the default above, then IMAP Junk/Spam auto-detection.": "Pozostaw automatyczne, aby użyć ustawienia powyżej, a następnie automatycznego wykrywania IMAP Junk/Spam.",
"Archive": "Archiwizuj",
"Sent, Trash, Spam/Junk and Archive are used directly by mail actions. The spam destination resolves in this order: account override → browser default → IMAP Junk/Spam auto-detection.": "Foldery Wysłane, Kosz, Spam/Junk i Archiwum są używane bezpośrednio przez akcje poczty. Folder spamu jest wybierany kolejno: ustawienie konta → domyślne przeglądarki → automatyczne wykrywanie IMAP Junk/Spam.",
"Mailbox storage": "Pamięć skrzynki",
"Loading quota…": "Ładowanie limitu…",
"Save folder settings": "Zapisz ustawienia folderów",
"Sender identities and account-wide signatures. Editing opens in a dedicated window so the form never collides with Settings.": "Tożsamości nadawcy i podpisy konta. Edycja otwiera się w osobnym oknie, aby formularz nie kolidował z Ustawieniami.",
"New identity": "Nowa tożsamość",
"Configured identities": "Skonfigurowane tożsamości",
"Select an identity to edit its sender data, text/HTML footer, image and OpenPGP defaults.": "Wybierz tożsamość, aby edytować dane nadawcy, stopkę tekstową/HTML, obraz i domyślne ustawienia OpenPGP.",
"Manage per-user keys, trust and discovery. Private keys never leave the server unless you explicitly export one.": "Zarządzaj kluczami użytkownika, zaufaniem i wyszukiwaniem. Klucze prywatne nie opuszczają serwera, chyba że jawnie je wyeksportujesz.",
"Refresh": "Odśwież",
"Policy & discovery": "Polityka i wyszukiwanie",
"Discover keys with WKD": "Wyszukuj klucze przez WKD",
"Fallback to configured HTTPS key server": "Użyj skonfigurowanego serwera kluczy HTTPS jako zapasowego",
"Encrypt only to trusted keys": "Szyfruj tylko do zaufanych kluczy",
"Save OpenPGP policy": "Zapisz politykę OpenPGP",
"Generate key": "Generuj klucz",
"Name": "Nazwa",
"Passphrase": "Hasło",
"Not stored by RustBox.": "Nie jest przechowywane przez RustBox.",
"Import / discover": "Import / wyszukiwanie",
"Armored public/private key": "Klucz publiczny/prywatny ASCII-armored",
"Initial trust": "Początkowe zaufanie",
"Unknown": "Nieznane",
"Verified": "Zweryfikowane",
"Trusted": "Zaufane",
"Untrusted": "Niezaufane",
"Import": "Importuj",
"Import file": "Importuj plik",
"Discover by email": "Wyszukaj po e-mailu",
"Discover key": "Wyszukaj klucz",
"Keyring": "Brelok kluczy",
"No keys loaded": "Brak wczytanych kluczy",
"Desktop notifications": "Powiadomienia systemowe",
"Sound for new mail and calendar reminders": "Dźwięk dla nowej poczty i przypomnień kalendarza",
"Do Not Disturb": "Nie przeszkadzać",
"DND from": "DND od",
"DND until": "DND do",
"Browser permission: unknown": "Uprawnienie przeglądarki: nieznane",
"Request permission": "Poproś o uprawnienie",
"Test notification": "Testuj powiadomienie",
"Save": "Zapisz",
"ManageSieve filters": "Filtry ManageSieve",
"Not checked": "Niesprawdzono",
"Backup": "Kopia zapasowa",
"Restore": "Przywróć",
"Scripts": "Skrypty",
"New script": "Nowy skrypt",
"Script name": "Nazwa skryptu",
"Rules in this script": "Reguły w tym skrypcie",
"Existing": "Istniejące",
"filters and RustBox rules are parsed automatically.": "filtry i reguły RustBox są analizowane automatycznie.",
"No rules found in this script": "Brak reguł w tym skrypcie",
"Create filter": "Utwórz filtr",
"Rules are edited in a separate visual window with a live “what will happen” preview.": "Reguły są edytowane w osobnym oknie wizualnym z podglądem „co się stanie”.",
"Autoresponder / vacation": "Autoresponder / urlop",
"Reply interval (days)": "Odstęp odpowiedzi (dni)",
"Active from": "Aktywne od",
"Active until": "Aktywne do",
"Message": "Wiadomość",
"Skip mailing lists": "Pomiń listy mailingowe",
"Skip automated messages": "Pomiń wiadomości automatyczne",
"Append autoresponder": "Dodaj autoresponder",
"Advanced Sieve source": "Zaawansowane źródło Sieve",
"Use this only for syntax the visual editor does not understand. Parsed rules stay editable above.": "Używaj tylko dla składni, której edytor wizualny nie rozumie. Rozpoznane reguły nadal można edytować powyżej.",
"Validate": "Sprawdź",
"Activate": "Aktywuj",
"Deactivate all": "Dezaktywuj wszystkie",
"Live capability checks for the current account. Credentials and server host names are not returned to the browser.": "Bieżące sprawdzanie możliwości aktualnego konta. Dane logowania i nazwy hostów serwera nie są zwracane do przeglądarki.",
"Reconnect": "Połącz ponownie",
"Realtime": "Czas rzeczywisty",
"Disconnected": "Rozłączono",
"IMAP IDLE events are delivered to the browser through the RustBox WebSocket connection.": "Zdarzenia IMAP IDLE są dostarczane do przeglądarki przez połączenie WebSocket RustBox.",
"Synchronization cache": "Pamięć synchronizacji",
"Search index": "Indeks wyszukiwania",
"Rebuild index": "Przebuduj indeks",
"Accounts signed in to this RustBox browser session.": "Konta zalogowane w tej sesji przeglądarki RustBox.",
"Add account": "Dodaj konto",
"Active sessions": "Aktywne sesje",
"Live RustBox sessions for this account, including other browsers while the server is running.": "Aktywne sesje RustBox dla tego konta, także w innych przeglądarkach, gdy serwer działa.",
"Revoke other sessions": "Unieważnij pozostałe sesje",
"Webmail based on the Oxi codebase, with a RustBox-owned interface compiled into the Rust executable.": "Webmail oparty na kodzie Oxi, z interfejsem RustBox kompilowanym bezpośrednio do programu Rust.",
"Upstream": "Projekt bazowy",
"Oxi base": "Baza Oxi",
"Frontend": "Frontend",
"HTML, CSS and vanilla JavaScript embedded at Cargo build time": "HTML, CSS i czysty JavaScript osadzane podczas kompilacji Cargo",
"Author": "Autor",
"Keyboard shortcuts": "Skróty klawiaturowe",
"Identity & signature": "Tożsamość i podpis",
"Sender identity": "Tożsamość nadawcy",
"Address and headers used when this identity is selected in the composer.": "Adres i nagłówki używane po wybraniu tej tożsamości w edytorze.",
"Display name": "Nazwa wyświetlana",
"Reply-To": "Odpowiedź do",
"Automatic Bcc": "Automatyczne UDW",
"Organization": "Organizacja",
"Signature / footer": "Podpis / stopka",
"Maintain separate text and HTML versions. Imported images are embedded into outgoing mail as inline CID images.": "Utrzymuj osobne wersje tekstową i HTML. Importowane obrazy są osadzane w wysyłanej poczcie jako obrazy CID inline.",
"Plain-text signature / footer": "Podpis / stopka tekstowa",
"HTML signature / footer": "Podpis / stopka HTML",
"You can also paste an image directly into this editor.": "Możesz też wkleić obraz bezpośrednio do edytora.",
"Signature position": "Pozycja podpisu",
"Above quoted text": "Nad cytowanym tekstem",
"Below quoted text": "Pod cytowanym tekstem",
"OpenPGP key": "Klucz OpenPGP",
"Automatic / none": "Automatycznie / brak",
"Defaults": "Domyślne",
"Choose where this signature is inserted and, when OpenPGP is configured, its default behavior.": "Wybierz miejsce wstawiania podpisu oraz domyślne zachowanie OpenPGP, jeśli jest skonfigurowane.",
"Signature on new messages": "Podpis w nowych wiadomościach",
"Signature on replies": "Podpis w odpowiedziach",
"Signature on forwards": "Podpis przy przekazywaniu",
"Default identity": "Domyślna tożsamość",
"Sign by default with OpenPGP": "Domyślnie podpisuj przez OpenPGP",
"Encrypt by default with OpenPGP": "Domyślnie szyfruj przez OpenPGP",
"Use inline OpenPGP by default": "Domyślnie używaj OpenPGP inline",
"Save identity": "Zapisz tożsamość",
"Build the rule visually and verify the preview before applying it.": "Zbuduj regułę wizualnie i sprawdź podgląd przed zastosowaniem.",
"Rule": "Reguła",
"Give it a readable name and decide whether it is active.": "Nadaj czytelną nazwę i zdecyduj, czy reguła ma być aktywna.",
"Rule name": "Nazwa reguły",
"Rule enabled": "Reguła włączona",
"Stop after this rule": "Zatrzymaj po tej regule",
"When": "Kiedy",
"Choose what the incoming message must match.": "Wybierz, co ma pasować w przychodzącej wiadomości.",
"Condition": "Warunek",
"From address": "Adres nadawcy",
"From domain": "Domena nadawcy",
"To / Cc address": "Adres Do / DW",
"From header": "Nagłówek From",
"To header": "Nagłówek To",
"Custom header": "Własny nagłówek",
"Match": "Dopasowanie",
"contains": "zawiera",
"is exactly": "jest dokładnie",
"wildcard matches": "pasuje do wzorca",
"Header name": "Nazwa nagłówka",
"Value": "Wartość",
"NOT / negate": "NIE / negacja",
"Join next condition": "Połącz następny warunek",
"Second condition": "Drugi warunek",
"None": "Brak",
"Join third condition": "Połącz trzeci warunek",
"Third condition": "Trzeci warunek",
"+ Add condition": "+ Dodaj warunek",
"Then": "Wtedy",
"Actions run in the order shown below.": "Akcje są wykonywane w kolejności pokazanej poniżej.",
"Action": "Akcja",
"Move to folder": "Przenieś do folderu",
"Copy to folder": "Kopiuj do folderu",
"Redirect": "Przekieruj",
"Redirect and keep copy": "Przekieruj i zachowaj kopię",
"Discard": "Odrzuć",
"Mark read": "Oznacz jako przeczytaną",
"Add IMAP flag": "Dodaj flagę IMAP",
"Set IMAP flags": "Ustaw flagi IMAP",
"Keep": "Zachowaj",
"Folder": "Folder",
"Second action": "Druga akcja",
"+ Add action": "+ Dodaj akcję",
"What will happen": "Co się stanie",
"Live explanation of the rule before you save it.": "Opis działania reguły przed zapisaniem.",
"Ready": "Gotowe",
"Complete the rule to see its effect.": "Uzupełnij regułę, aby zobaczyć jej efekt.",
"Add rule": "Dodaj regułę",
"Update selected rule": "Aktualizuj wybraną regułę",
"Clear": "Wyczyść",
"Global": "Globalne",
"Focus search": "Przejdź do wyszukiwania",
"Toggle selection mode": "Przełącz tryb zaznaczania",
"Show shortcuts": "Pokaż skróty",
"Next / previous message": "Następna / poprzednia wiadomość",
"Select / unselect current message": "Zaznacz / odznacz bieżącą wiadomość",
"Reply": "Odpowiedz",
"Reply all": "Odpowiedz wszystkim",
"Forward": "Przekaż",
"Read / unread": "Przeczytana / nieprzeczytana",
"Flag / unflag": "Oznacz / usuń oznaczenie",
"Spam / not spam": "Spam / nie spam",
"Delete / move to Trash": "Usuń / przenieś do Kosza",
"Close": "Zamknij",
"Label": "Etykieta",
"Color": "Kolor",
"Advanced search": "Wyszukiwanie zaawansowane",
"Contains": "Zawiera",
"All folders": "Wszystkie foldery",
"Date from": "Data od",
"Date to": "Data do",
"Read status": "Stan odczytu",
"Any": "Dowolny",
"Unread": "Nieprzeczytane",
"Read": "Przeczytane",
"Flag": "Oznacz gwiazdką",
"Flagged": "Oznaczone",
"Not flagged": "Nieoznaczone",
"Attachments": "Załączniki",
"Has attachment": "Ma załącznik",
"No attachment": "Bez załącznika",
"Sort": "Sortowanie",
"Saved searches": "Zapisane wyszukiwania",
"Reset": "Wyczyść",
"Save search": "Zapisz wyszukiwanie",
"Search": "Szukaj",
"Contacts": "Kontakty",
"All contacts": "Wszystkie kontakty",
"New group": "Nowa grupa",
"New": "Nowy",
"Import vCard": "Importuj vCard",
"Export vCard": "Eksportuj vCard",
"Find duplicates": "Znajdź duplikaty",
"Photo": "Zdjęcie",
"Upload photo": "Wgraj zdjęcie",
"Remove photo": "Usuń zdjęcie",
"Other emails": "Inne adresy e-mail",
"Phone": "Telefon",
"Other phones": "Inne telefony",
"Company": "Firma",
"Job title": "Stanowisko",
"Website": "Strona WWW",
"Birthday": "Urodziny",
"Address": "Adres",
"Other addresses": "Inne adresy",
"Notes": "Notatki",
"Favorite": "Ulubiony",
"Groups": "Grupy",
"Duplicate contacts": "Duplikaty kontaktów",
"Duplicates are detected when two contacts share a primary or alternate e-mail address. Choose which record to keep.": "Duplikaty są wykrywane, gdy dwa kontakty mają ten sam główny lub dodatkowy adres e-mail. Wybierz rekord do zachowania.",
"Calendar": "Kalendarz",
"Today": "Dzisiaj",
"Previous": "Poprzedni",
"Next": "Następny",
"Month": "Miesiąc",
"Week": "Tydzień",
"Day": "Dzień",
"Agenda": "Agenda",
"Import ICS": "Importuj ICS",
"Export ICS": "Eksportuj ICS",
"New event": "Nowe wydarzenie",
"Title": "Tytuł",
"All day": "Cały dzień",
"Start": "Początek",
"End": "Koniec",
"Recurrence": "Powtarzanie",
"Does not repeat": "Nie powtarza się",
"Daily": "Codziennie",
"Weekly": "Co tydzień",
"Monthly": "Co miesiąc",
"Yearly": "Co rok",
"Custom RRULE": "Własna reguła RRULE",
"Status": "Status",
"Confirmed": "Potwierdzone",
"Tentative": "Wstępne",
"Cancelled": "Anulowane",
"Reminder": "Przypomnienie",
"At start": "Na początku",
"5 minutes": "5 minut",
"15 minutes": "15 minut",
"30 minutes": "30 minut",
"1 hour": "1 godzina",
"1 day": "1 dzień",
"Organizer": "Organizator",
"Attendees": "Uczestnicy",
"Location": "Lokalizacja",
"Meeting link": "Link do spotkania",
"Description": "Opis",
"Message headers": "Nagłówki wiadomości",
"Copy": "Kopiuj",
"Message source": "Źródło wiadomości",
"Copy source": "Kopiuj źródło",
"Download .eml": "Pobierz .eml",
"Show full addresses": "Pokaż pełne adresy",
"Collapse addresses": "Zwiń adresy",
"Close folders": "Zamknij foldery",
"Back to messages": "Wróć do wiadomości",
"Manage folders": "Zarządzaj folderami",
"New label": "Nowa etykieta",
"Mail folders": "Foldery poczty",
"Message labels": "Etykiety wiadomości",
"Sign out": "Wyloguj się",
"Recover a saved draft": "Odzyskaj zapisany szkic",
"Toggle full screen composer": "Przełącz pełny ekran edytora",
"Full screen": "Pełny ekran",
"Formatting": "Formatowanie",
"Bold": "Pogrubienie",
"Italic": "Kursywa",
"Underline": "Podkreślenie",
"Strikethrough": "Przekreślenie",
"Bullet list": "Lista punktowana",
"Numbered list": "Lista numerowana",
"Quote": "Cytat",
"Align left": "Wyrównaj do lewej",
"Align center": "Wyśrodkuj",
"Align right": "Wyrównaj do prawej",
"Insert link": "Wstaw link",
"Insert inline image": "Wstaw obraz w treści",
"Edit selected image": "Edytuj zaznaczony obraz",
"Clear formatting": "Wyczyść formatowanie",
"Undo": "Cofnij",
"Redo": "Ponów",
"Attach files": "Dołącz pliki",
"Preview Markdown": "Podgląd Markdown",
"Insert saved response": "Wstaw zapisaną odpowiedź",
"Write a message": "Napisz wiadomość",
"Markdown preview": "Podgląd Markdown",
"Recovered draft": "Odzyskany szkic",
"Forward as attachment": "Przekaż jako załącznik",
"Open": "Otwórz",
"Mark unread": "Oznacz jako nieprzeczytaną",
"Unflag": "Usuń oznaczenie",
"Move to…": "Przenieś do…",
"Copy to…": "Kopiuj do…",
"Add label…": "Dodaj etykietę…",
"Remove label…": "Usuń etykietę…",
"Create filter from sender": "Utwórz filtr z nadawcy",
"Create filter from domain": "Utwórz filtr z domeny",
"Create filter from subject": "Utwórz filtr z tematu",
"Add sender to contacts": "Dodaj nadawcę do kontaktów",
"Search messages from sender": "Szukaj wiadomości od nadawcy",
"Unsubscribe from mailing list": "Wypisz się z listy mailingowej",
"Show headers": "Pokaż nagłówki",
"Show source": "Pokaż źródło",
"Copy Message-ID": "Kopiuj Message-ID",
"Copy sender address": "Kopiuj adres nadawcy",
"Print": "Drukuj",
"Choose folder": "Wybierz folder",
"Compose to sender": "Napisz do nadawcy",
"Add to contacts": "Dodaj do kontaktów",
"Stop trusting sender": "Przestań ufać nadawcy",
"Trust sender and load remote images": "Ufaj nadawcy i ładuj zdalne obrazy",
"Copy address": "Kopiuj adres",
"Search messages": "Szukaj wiadomości",
"Draft saved": "Szkic zapisany",
"Uploading files…": "Wysyłanie plików…",
"Upload complete": "Wysyłanie zakończone",
"Allow UI scaling for predefined style": "Zezwól na skalowanie interfejsu dla gotowego stylu",
"When enabled, the UI size control can override the scale defined by the selected preset.": "Po włączeniu ustawienie rozmiaru interfejsu może nadpisać skalę wybranego stylu.",
"Maximum attachment size": "Maksymalny rozmiar załącznika",
"Server-wide limit set with": "Globalny limit serwera ustawiany przez",
"in rustbox.toml.": "w rustbox.toml.",
"Uploading attachment…": "Wysyłanie załącznika…",
"maximum attachment size is": "maksymalny rozmiar załącznika to",
"Attachment uploaded": "Załącznik wysłany",
"attachments uploaded": "załączników wysłano",
"Upload failed": "Wysyłanie nie powiodło się",
"Another upload is already in progress": "Inne wysyłanie jest już w toku",
"Repository": "Repozytorium",
"Fastmail inspired": "Inspirowany Fastmail",
"Apple Mail inspired": "Inspirowany Apple Mail",
"Yahoo Mail inspired": "Inspirowany Yahoo Mail",
"HEY inspired": "Inspirowany HEY",
"Maximum message size": "Maksymalny rozmiar wiadomości",
"Per-file upload limit set with": "Limit pojedynczego pliku ustawiany przez",
"Hard limit for the complete RFC822/MIME message sent to the MTA, including base64/MIME overhead.": "Twardy limit całej wiadomości RFC822/MIME wysyłanej do MTA, wraz z narzutem base64/MIME.",
"Estimated final message size": "Szacowany końcowy rozmiar wiadomości",
"Estimated size. The exact RFC822/MIME size is checked by the server before SMTP delivery.": "Rozmiar szacowany. Dokładny rozmiar RFC822/MIME jest sprawdzany przez serwer przed wysłaniem przez SMTP.",
"Estimated message size exceeds the configured limit": "Szacowany rozmiar wiadomości przekracza skonfigurowany limit",
"The exact size will be checked before sending.": "Dokładny rozmiar zostanie sprawdzony przed wysłaniem."
},
"fixed_interface_style_note": "stały styl interfejsu. Motyw nadal przełącza jasny/ciemny; wybierz RustBox (niestandardowy), aby edytować kolory, odstępy i geometrię.",
"custom_interface_style_note": "RustBox można w pełni dostosować: kolory, odstępy, panele, typografię i zaawansowane ustawienia interfejsu.",
"reply": "Odpowiedz",
"reply_all": "Odpowiedz wszystkim",
"forward": "Przekaż",
"headers": "Nagłówki",
"message_details": "Szczegóły",
"move_copy": "Przenieś / kopiuj",
"move_to": "Przenieś do…",
"copy_to": "Kopiuj do…"
}
+86 -45
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" data-base-path="__RUSTBOX_BASE_PATH__" data-theme="dark" data-density="compact" data-preview="right">
<html lang="en" data-base-path="__RUSTBOX_BASE_PATH__" data-default-theme="__RUSTBOX_DEFAULT_THEME__" data-default-interface-skin="__RUSTBOX_DEFAULT_INTERFACE_SKIN__" data-max-attachment-bytes="__RUSTBOX_MAX_ATTACHMENT_BYTES__" data-max-attachment-mb="__RUSTBOX_MAX_ATTACHMENT_MB__" data-max-message-bytes="__RUSTBOX_MAX_MESSAGE_BYTES__" data-max-message-mb="__RUSTBOX_MAX_MESSAGE_MB__" data-theme="dark" data-interface-skin="__RUSTBOX_DEFAULT_INTERFACE_SKIN__" data-density="compact" data-preview="right">
<head>
<meta charset="utf-8">
@@ -7,6 +7,16 @@
<meta name="color-scheme" content="dark light">
<meta name="description" content="RustBox webmail">
<title>RustBox</title>
<script>
(() => {
const root = document.documentElement;
const configured = root.dataset.defaultTheme || "system";
root.dataset.theme = configured === "system"
? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark")
: configured;
root.dataset.interfaceSkin = root.dataset.defaultInterfaceSkin || "rustbox";
})();
</script>
<link rel="icon" href="__RUSTBOX_ASSET_PREFIX__/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="__RUSTBOX_ASSET_PREFIX__/assets/app.__RUSTBOX_ASSET_ID__.css">
</head>
@@ -19,7 +29,7 @@
<strong>RustBox</strong>
</div>
<section id="login-view" class="login-view hidden" aria-label="Sign in">
<section id="login-view" class="login-view hidden" data-i18n-scope aria-label="Sign in">
<div class="login-panel">
<header class="login-brand">
<svg class="brand-icon" aria-hidden="true">
@@ -43,7 +53,7 @@
<main id="app-view" class="app-view hidden">
<div id="offline-banner" class="offline-banner hidden" role="status" aria-live="polite"></div>
<header class="appbar">
<header class="appbar" data-i18n-scope>
<div class="app-brand">
<svg class="brand-icon" aria-hidden="true">
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-mail"></use>
@@ -82,7 +92,7 @@
<div class="mail-shell">
<button id="folder-drawer-scrim" class="folder-drawer-scrim" type="button" aria-label="Close folders"
tabindex="-1"></button>
<aside id="folder-pane" class="folder-pane">
<aside id="folder-pane" class="folder-pane" data-i18n-scope>
<div class="folder-drawer-header mobile-only">
<button id="folder-drawer-close" class="icon-button folder-drawer-close" type="button"
aria-label="Back to messages" title="Back to messages"><svg>
@@ -105,6 +115,10 @@
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-plus"></use>
</svg></button></div>
<nav id="tag-list" class="folder-list tag-list" aria-label="Message labels"></nav>
<div id="folder-quota" class="folder-quota" aria-live="polite">
<div class="folder-quota-label"><span>IMAP storage</span><span id="folder-quota-text">Loading…</span></div>
<div class="folder-quota-track"><span id="folder-quota-bar"></span></div>
</div>
<div class="folder-pane-footer"><span id="mail-server-label">IMAP</span><span>RustBox __RUSTBOX_VERSION__</span>
</div>
<div id="folder-resizer" class="pane-resizer" role="separator" aria-orientation="vertical"
@@ -193,15 +207,22 @@
</svg></button>
<button id="reply-btn" class="button quiet" type="button"><svg>
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-reply"></use>
</svg><span>Reply</span></button>
</svg><span data-i18n="reply">Reply</span></button>
<button id="reply-all-btn" class="button quiet" type="button"><svg>
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-reply-all"></use>
</svg><span>Reply all</span></button>
</svg><span data-i18n="reply_all">Reply all</span></button>
<button id="forward-btn" class="button quiet" type="button"><svg>
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-forward"></use>
</svg><span>Forward</span></button>
<button id="headers-btn" class="button quiet" type="button">Headers</button><button id="source-btn"
class="button quiet" type="button" data-i18n="source">Source</button>
</svg><span data-i18n="forward">Forward</span></button>
<details id="message-details-menu" class="toolbar-menu">
<summary class="button quiet"><svg aria-hidden="true">
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-file-text"></use>
</svg><span data-i18n="message_details">Message details</span></summary>
<div class="toolbar-menu-popover" role="menu">
<button id="headers-btn" type="button" role="menuitem" data-i18n="headers">Headers</button>
<button id="source-btn" type="button" role="menuitem" data-i18n="source">Source</button>
</div>
</details>
<button id="quick-filter-btn" class="icon-button" type="button" title="Create filter from sender"
aria-label="Create filter from sender" data-i18n-title="create_filter_sender"
data-i18n-aria-label="create_filter_sender"><svg>
@@ -225,12 +246,15 @@
aria-label="Mark read or unread"><svg>
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-mail"></use>
</svg></button>
<label class="move-control"><span class="sr-only">Move to folder</span><select id="move-select">
<option value="">Move to…</option>
</select></label>
<label class="move-control"><span class="sr-only">Copy to folder</span><select id="copy-select">
<option value="">Copy to…</option>
</select></label>
<details id="message-transfer-menu" class="toolbar-menu">
<summary class="button quiet"><svg aria-hidden="true">
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-folder"></use>
</svg><span data-i18n="move_copy">Move / copy</span></summary>
<div class="toolbar-menu-popover" role="menu">
<button id="reader-move-action" type="button" role="menuitem" data-i18n="move_to">Move to…</button>
<button id="reader-copy-action" type="button" role="menuitem" data-i18n="copy_to">Copy to…</button>
</div>
</details>
<span class="toolbar-spacer"></span>
<button id="delete-btn" class="icon-button danger" type="button" title="Delete" aria-label="Delete"><svg>
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-trash"></use>
@@ -246,7 +270,7 @@
aria-label="Add sender to contacts"><svg>
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-user-round"></use>
</svg></button></div>
<div id="reader-to" class="reader-recipients"></div>
<div id="reader-to" class="reader-recipients" role="button" tabindex="0" aria-expanded="false" title="Show full addresses"></div>
</div><time id="reader-date"></time>
</div>
</header>
@@ -275,7 +299,7 @@
</div>
</main>
<dialog id="compose-dialog" class="dialog compose-dialog">
<dialog id="compose-dialog" class="dialog compose-dialog" data-i18n-scope>
<form id="compose-form" class="dialog-card compose-card" method="dialog">
<header class="dialog-header"><strong id="compose-title">New message</strong>
<div class="compose-header-actions"><button type="button" id="compose-recover" class="button quiet small"
@@ -410,28 +434,31 @@
<button type="button" id="compose-response-insert" title="Insert saved response" disabled><svg>
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-file-text"></use>
</svg></button>
<label class="compose-mode-control"><span class="sr-only">Compose format</span><select id="compose-mode">
<option value="html">HTML</option>
<option value="text">Plain text</option>
<option value="markdown">Markdown</option>
</select></label>
</div>
<div id="compose-editor" class="compose-editor" contenteditable="true" role="textbox" aria-multiline="true"></div>
<div id="compose-editor" class="compose-editor" contenteditable="true" role="textbox" aria-multiline="true"
data-placeholder="Write a message"></div>
<div id="compose-upload-progress" class="compose-upload-progress hidden" role="status" aria-live="polite">
<div class="compose-upload-track" aria-hidden="true"><span id="compose-upload-bar"></span></div><span
id="compose-upload-label">Uploading…</span>
</div>
<div id="compose-attachments" class="compose-attachments hidden"></div>
<div id="compose-error" class="form-error hidden"></div>
<footer class="dialog-actions"><span id="compose-status" class="muted"></span><button type="button"
class="button quiet close-dialog">Cancel</button><button id="send-btn" type="submit"
class="button primary"><svg>
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-send"></use>
</svg><span>Send</span></button></footer>
<footer class="dialog-actions compose-footer">
<label class="compose-mode-control"><span class="sr-only">Compose format</span><select id="compose-mode">
<option value="html">HTML</option>
<option value="text">Plain text</option>
<option value="markdown">Markdown</option>
</select></label>
<span id="compose-status" class="muted"></span><span id="compose-message-size" class="muted compose-message-size" title="Estimated final message size">~0 / __RUSTBOX_MAX_MESSAGE_MB__ MiB</span>
<div class="compose-submit-actions"><button type="button" class="button quiet close-dialog">Cancel</button><button
id="send-btn" type="submit" class="button primary"><svg>
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-send"></use>
</svg><span>Send</span></button></div>
</footer>
</form>
</dialog>
<dialog id="markdown-preview-dialog" class="dialog markdown-preview-dialog">
<dialog id="markdown-preview-dialog" class="dialog markdown-preview-dialog" data-i18n-scope>
<div class="dialog-card markdown-preview-card">
<header class="dialog-header"><strong>Markdown preview</strong><button type="button"
class="icon-button close-dialog" aria-label="Close"><svg>
@@ -490,7 +517,7 @@
</form>
</dialog>
<dialog id="folder-dialog" class="dialog">
<dialog id="folder-dialog" class="dialog" data-i18n-scope>
<div class="dialog-card folder-card">
<header class="dialog-header"><strong>Folders</strong><button type="button" class="icon-button close-dialog"
aria-label="Close"><svg>
@@ -509,7 +536,7 @@
</div>
</dialog>
<dialog id="settings-dialog" class="dialog settings-dialog">
<dialog id="settings-dialog" class="dialog settings-dialog" data-i18n-scope>
<div class="dialog-card settings-card">
<header class="dialog-header"><strong>Settings</strong><button type="button" class="icon-button close-dialog"
aria-label="Close"><svg>
@@ -548,15 +575,27 @@
<option value="system">System</option>
</select></label>
<label><span>Interface style</span><select id="pref-interface-skin">
<option value="default">Global default</option>
<option value="rustbox">RustBox (custom)</option>
<option value="roundcube-classic">Roundcube Classic</option>
<option value="roundcube-larry">Roundcube Larry</option>
<option value="roundcube-elastic">Roundcube Elastic</option>
<option value="clean">Clean</option>
<option value="gmail">Gmail inspired</option>
<option value="proton">Proton inspired</option>
<option value="zoho">Zoho inspired</option>
<option value="outlook">Outlook inspired</option>
<option value="minimal">Minimal</option>
<option value="graphite">Graphite</option>
<option value="high-contrast">High contrast</option>
<option value="fastmail">Fastmail inspired</option>
<option value="apple-mail">Apple Mail inspired</option>
<option value="yahoo">Yahoo Mail inspired</option>
<option value="hey">HEY inspired</option>
<option value="nord">Nord</option>
</select><small id="pref-interface-skin-note" class="muted">RustBox is fully customizable: colors,
spacing, panes and typography.</small></label>
<label id="pref-preset-scale-unlocked-row" class="checkbox-row hidden"><input id="pref-preset-scale-unlocked" type="checkbox"><span>Allow UI scaling for predefined style</span><small class="muted">When enabled, the UI size control can override the scale defined by the selected preset.</small></label>
<label><span data-i18n="color_preset">Color preset</span><select id="pref-color-preset">
<option value="sage">Sage</option>
<option value="blue">Blue</option>
@@ -819,6 +858,8 @@
message text suggests a missing attachment</span></label>
<label class="checkbox-row"><input id="compose-pref-spellcheck" type="checkbox"><span>Use browser
spellcheck while composing</span></label>
<label><span>Maximum attachment size</span><output id="compose-pref-max-attachment">__RUSTBOX_MAX_ATTACHMENT_MB__ MiB</output><small class="muted">Per-file upload limit set with <code>max_attachment_size_mb</code> in rustbox.toml.</small></label>
<label><span>Maximum message size</span><output id="compose-pref-max-message">__RUSTBOX_MAX_MESSAGE_MB__ MiB</output><small class="muted">Hard limit for the complete RFC822/MIME message sent to the MTA, including base64/MIME overhead.</small></label>
</div>
<p class="muted">Markdown is stored as source text and sent as a safe rendered HTML alternative with a
plain-text fallback.</p>
@@ -1032,12 +1073,10 @@
</div>
<div class="diagnostics-grid">
<section class="diagnostic-card">
<div class="section-heading"><span>IMAP</span>
<div class="inline-actions"><strong id="diagnostic-imap-status">Not checked</strong><button
id="diagnostic-imap-reconnect" class="button quiet" type="button">Reconnect</button></div>
</div>
<div class="section-heading"><span>IMAP</span><strong id="diagnostic-imap-status">Not checked</strong></div>
<div id="diagnostic-imap-summary" class="capability-grid"></div>
<div id="diagnostic-imap-caps" class="capability-list"></div>
<div class="inline-actions diagnostic-imap-actions"><button id="diagnostic-imap-reconnect" class="button quiet" type="button">Reconnect</button></div>
</section>
<section class="diagnostic-card">
<div class="section-heading"><span>SMTP</span><strong id="diagnostic-smtp-status">Not checked</strong>
@@ -1099,6 +1138,8 @@
<dd>HTML, CSS and vanilla JavaScript embedded at Cargo build time</dd>
<dt>Author</dt>
<dd>@linuxiarz.pl</dd>
<dt>Repository</dt>
<dd><a href="https://git.linuxiarz.pl/gru/RustBox" target="_blank" rel="noopener noreferrer">git.linuxiarz.pl/gru/RustBox</a></dd>
</dl>
<div class="inline-actions"><button id="shortcuts-open" class="button quiet" type="button">Keyboard
shortcuts</button></div>
@@ -1110,7 +1151,7 @@
<dialog id="identity-dialog" class="dialog identity-dialog">
<dialog id="identity-dialog" class="dialog identity-dialog" data-i18n-scope>
<form id="identity-form" class="dialog-card identity-card">
<header class="dialog-header"><strong id="identity-dialog-title">Identity & signature</strong><button
type="button" class="icon-button close-dialog" aria-label="Close"><svg>
@@ -1197,7 +1238,7 @@
</form>
</dialog>
<dialog id="sieve-rule-dialog" class="dialog sieve-rule-dialog">
<dialog id="sieve-rule-dialog" class="dialog sieve-rule-dialog" data-i18n-scope>
<div class="dialog-card sieve-rule-card">
<header class="dialog-header">
<div><strong id="sieve-rule-dialog-title">Create filter</strong><span id="sieve-rule-dialog-subtitle"
@@ -1365,7 +1406,7 @@
</div>
</dialog>
<dialog id="shortcuts-dialog" class="dialog">
<dialog id="shortcuts-dialog" class="dialog" data-i18n-scope>
<div class="dialog-card shortcuts-card">
<header class="dialog-header"><strong>Keyboard shortcuts</strong><button type="button"
class="icon-button close-dialog" aria-label="Close"><svg>
@@ -1415,7 +1456,7 @@
</div>
</dialog>
<dialog id="tag-dialog" class="dialog">
<dialog id="tag-dialog" class="dialog" data-i18n-scope>
<div class="dialog-card tag-card">
<header class="dialog-header"><strong>Label</strong><button type="button" class="icon-button close-dialog"
aria-label="Close"><svg>
@@ -1433,7 +1474,7 @@
</div>
</dialog>
<dialog id="advanced-search-dialog" class="dialog">
<dialog id="advanced-search-dialog" class="dialog" data-i18n-scope>
<div class="dialog-card advanced-search-card">
<header class="dialog-header"><strong>Advanced search</strong><button type="button"
class="icon-button close-dialog" aria-label="Close"><svg>
@@ -1485,7 +1526,7 @@
</div>
</dialog>
<dialog id="contacts-dialog" class="dialog">
<dialog id="contacts-dialog" class="dialog" data-i18n-scope>
<div class="dialog-card contacts-card">
<header class="dialog-header"><strong>Contacts</strong><button type="button" class="icon-button close-dialog"
aria-label="Close"><svg>
@@ -1544,7 +1585,7 @@
</div>
</dialog>
<dialog id="contact-duplicates-dialog" class="dialog">
<dialog id="contact-duplicates-dialog" class="dialog" data-i18n-scope>
<div class="dialog-card duplicates-card">
<header class="dialog-header"><strong>Duplicate contacts</strong><button type="button"
class="icon-button close-dialog" aria-label="Close"><svg>
@@ -1557,7 +1598,7 @@
</div>
</dialog>
<dialog id="calendar-dialog" class="dialog">
<dialog id="calendar-dialog" class="dialog" data-i18n-scope>
<div class="dialog-card calendar-card">
<header class="dialog-header"><strong>Calendar</strong>
<div class="inline-actions"><button id="calendar-today" type="button" class="button quiet">Today</button><button
@@ -1620,7 +1661,7 @@
</form>
</div>
</dialog>
<dialog id="headers-dialog" class="dialog">
<dialog id="headers-dialog" class="dialog" data-i18n-scope>
<div class="dialog-card headers-card">
<header class="dialog-header"><strong>Message headers</strong><button type="button"
class="icon-button close-dialog" aria-label="Close"><svg>
@@ -1634,7 +1675,7 @@
</div>
</dialog>
<dialog id="add-account-dialog" class="dialog">
<dialog id="add-account-dialog" class="dialog" data-i18n-scope>
<div class="dialog-card add-account-card">
<header class="dialog-header"><strong>Add account</strong><button type="button" class="icon-button close-dialog"
aria-label="Close"><svg>
+1 -1
View File
@@ -68,7 +68,7 @@
imageEditorBlob: null,
draftSaveTimer: null,
prefs: {
theme: "system", interfaceSkin: "rustbox", density: "compact", preview: "right", scale: "normal", language: "en", composeFormat: "html",
theme: root.dataset.defaultTheme || "system", interfaceSkin: "default", presetScaleUnlocked: false, density: "compact", preview: "right", scale: "normal", language: "en", composeFormat: "html",
composeAutosaveMs: 3000, defaultPriority: "normal", requestMdnDefault: false, attachmentReminder: true, composeFontFamily: "system", replyPosition: "above", quoteStyle: "standard", forwardMode: "inline", spellcheckEnabled: true,
colorPreset: "sage", accentColor: "#6f9c86", backgroundColor: "#121513", panelColor: "#181c19", textColor: "#edf0ec", fontFamily: "system", fontSize: 13,
rowHeight: 58, folderPaneWidth: 228, messagePaneWidth: 420, borderRadius: 4,
+371 -304
View File
@@ -1,333 +1,400 @@
/* RustBox UI module: 10-core.js */
function setConnectivityState(online, detail = "") {
state.networkOnline = Boolean(online);
document.body.classList.toggle("network-offline", !state.networkOnline);
const banner = $("#offline-banner");
if (banner) {
banner.classList.toggle("hidden", state.networkOnline);
banner.textContent = detail || (navigator.onLine ? "RustBox cannot reach the server. Read-only cached content may still be visible." : "You are offline. RustBox will reconnect when the network returns.");
}
const label = $("#connection-label");
if (!label) return;
if (!state.networkOnline) label.textContent = navigator.onLine ? "Reconnecting" : "Offline";
else if (["Offline", "Reconnecting", "Connection error"].includes(label.textContent)) label.textContent = "Connected";
function setConnectivityState(online, detail = "") {
state.networkOnline = Boolean(online);
document.body.classList.toggle("network-offline", !state.networkOnline);
const banner = $("#offline-banner");
if (banner) {
banner.classList.toggle("hidden", state.networkOnline);
banner.textContent = detail || (navigator.onLine ? "RustBox cannot reach the server. Read-only cached content may still be visible." : "You are offline. RustBox will reconnect when the network returns.");
}
const label = $("#connection-label");
if (!label) return;
if (!state.networkOnline) label.textContent = navigator.onLine ? "Reconnecting" : "Offline";
else if (["Offline", "Reconnecting", "Connection error"].includes(label.textContent)) label.textContent = "Connected";
}
function listStateStorageKey(folder = state.folder) {
return `rustbox.listState.${state.accountId || "anonymous"}.${encodeURIComponent(folder || "INBOX")}`;
function listStateStorageKey(folder = state.folder) {
return `rustbox.listState.${state.accountId || "anonymous"}.${encodeURIComponent(folder || "INBOX")}`;
}
function lastFolderStorageKey() {
return `rustbox.lastFolder.${state.accountId || "anonymous"}`;
}
function activeSearchStorageKey() {
return `rustbox.activeSearch.${state.accountId || "anonymous"}`;
}
function saveActiveSearchContext() {
if (!state.searchContext) return sessionStorage.removeItem(activeSearchStorageKey());
sessionStorage.setItem(activeSearchStorageKey(), JSON.stringify(state.searchContext));
}
function readActiveSearchContext() {
try {
const parsed = JSON.parse(sessionStorage.getItem(activeSearchStorageKey()) || "null");
return parsed && typeof parsed.params === "string" ? parsed : null;
} catch (_) {
return null;
}
}
function lastFolderStorageKey() {
return `rustbox.lastFolder.${state.accountId || "anonymous"}`;
function clearActiveSearchContext() {
state.searchContext = null;
state.searchAllResults = null;
sessionStorage.removeItem(activeSearchStorageKey());
}
function readMessageListState(folder = state.folder) {
try {
const parsed = JSON.parse(localStorage.getItem(listStateStorageKey(folder)) || "null");
if (!parsed || typeof parsed !== "object") return null;
return {
scrollTop: Math.max(0, Number(parsed.scrollTop) || 0),
page: Math.max(0, Number(parsed.page) || 0),
};
} catch (_) {
return null;
}
}
function activeSearchStorageKey() {
return `rustbox.activeSearch.${state.accountId || "anonymous"}`;
function saveMessageListState() {
if (!state.accountId) return;
const list = $("#message-list");
if (!list) return;
if (state.searchContext) {
state.searchContext.scrollTop = list.scrollTop;
saveActiveSearchContext();
return;
}
localStorage.setItem(listStateStorageKey(), JSON.stringify({ scrollTop: list.scrollTop, page: state.page }));
localStorage.setItem(lastFolderStorageKey(), state.folder);
}
function saveActiveSearchContext() {
if (!state.searchContext) return sessionStorage.removeItem(activeSearchStorageKey());
sessionStorage.setItem(activeSearchStorageKey(), JSON.stringify(state.searchContext));
}
function scheduleMessageListStateSave() {
clearTimeout(state.listStateTimer);
state.listStateTimer = setTimeout(saveMessageListState, 120);
}
function readActiveSearchContext() {
async function request(path, options = {}) {
const headers = new Headers(options.headers || {});
if (state.accountId) headers.set("X-Active-Account", state.accountId);
if (options.body && !(options.body instanceof FormData)) headers.set("Content-Type", "application/json");
if (options.method && options.method !== "GET") headers.set("X-Requested-With", "XMLHttpRequest");
const method = String(options.method || "GET").toUpperCase();
const maxAttempts = method === "GET" || method === "HEAD" ? 3 : 1;
let lastNetworkError = null;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
try {
const parsed = JSON.parse(sessionStorage.getItem(activeSearchStorageKey()) || "null");
return parsed && typeof parsed.params === "string" ? parsed : null;
} catch (_) {
return null;
}
}
function clearActiveSearchContext() {
state.searchContext = null;
state.searchAllResults = null;
sessionStorage.removeItem(activeSearchStorageKey());
}
function readMessageListState(folder = state.folder) {
try {
const parsed = JSON.parse(localStorage.getItem(listStateStorageKey(folder)) || "null");
if (!parsed || typeof parsed !== "object") return null;
return {
scrollTop: Math.max(0, Number(parsed.scrollTop) || 0),
page: Math.max(0, Number(parsed.page) || 0),
};
} catch (_) {
return null;
}
}
function saveMessageListState() {
if (!state.accountId) return;
const list = $("#message-list");
if (!list) return;
if (state.searchContext) {
state.searchContext.scrollTop = list.scrollTop;
saveActiveSearchContext();
return;
}
localStorage.setItem(listStateStorageKey(), JSON.stringify({ scrollTop: list.scrollTop, page: state.page }));
localStorage.setItem(lastFolderStorageKey(), state.folder);
}
function scheduleMessageListStateSave() {
clearTimeout(state.listStateTimer);
state.listStateTimer = setTimeout(saveMessageListState, 120);
}
async function request(path, options = {}) {
const headers = new Headers(options.headers || {});
if (state.accountId) headers.set("X-Active-Account", state.accountId);
if (options.body && !(options.body instanceof FormData)) headers.set("Content-Type", "application/json");
if (options.method && options.method !== "GET") headers.set("X-Requested-With", "XMLHttpRequest");
const method = String(options.method || "GET").toUpperCase();
const maxAttempts = method === "GET" || method === "HEAD" ? 3 : 1;
let lastNetworkError = null;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
try {
const response = await fetch(apiPath(path), {
credentials: "include",
...options,
headers,
});
if ([502, 503, 504].includes(response.status) && attempt + 1 < maxAttempts) {
await sleep(250 * (attempt + 1));
continue;
}
const contentType = response.headers.get("content-type") || "";
const payload = contentType.includes("application/json")
? await response.json().catch(() => ({}))
: await response.text().catch(() => "");
if (!response.ok) {
const message = payload?.error?.message || (typeof payload === "string" && payload) || `Request failed (${response.status})`;
const error = new Error(message);
error.status = response.status;
throw error;
}
setConnectivityState(true);
return payload;
} catch (error) {
if (error?.status) throw error;
lastNetworkError = error;
setConnectivityState(false);
if (attempt + 1 < maxAttempts) {
await sleep(350 * (attempt + 1));
continue;
}
}
}
const error = new Error(navigator.onLine ? "RustBox server is temporarily unreachable" : "You are offline");
error.cause = lastNetworkError;
error.network = true;
throw error;
}
function uploadRequest(path, formData, onProgress = null) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", apiPath(path), true);
xhr.withCredentials = true;
if (state.accountId) xhr.setRequestHeader("X-Active-Account", state.accountId);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
if (typeof onProgress === "function") {
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable && event.total > 0) onProgress(Math.round((event.loaded / event.total) * 100));
});
}
xhr.addEventListener("error", () => reject(new Error("Upload failed: network error")));
xhr.addEventListener("abort", () => reject(new Error("Upload cancelled")));
xhr.addEventListener("load", () => {
const contentType = xhr.getResponseHeader("content-type") || "";
let payload = xhr.responseText || "";
if (contentType.includes("application/json")) {
try { payload = payload ? JSON.parse(payload) : {}; } catch (_) { payload = {}; }
}
if (xhr.status < 200 || xhr.status >= 300) {
const message = payload?.error?.message || (typeof payload === "string" && payload) || `Upload failed (${xhr.status})`;
const error = new Error(message);
error.status = xhr.status;
reject(error);
return;
}
resolve(payload);
const response = await fetch(apiPath(path), {
credentials: "include",
...options,
headers,
});
xhr.send(formData);
});
}
function toast(message, kind = "info", duration = null, action = null) {
const normalized = ["success", "error", "warning", "info"].includes(kind) ? kind : "info";
const lifetime = Number(duration) > 0 ? Number(duration) : normalized === "error" ? 6500 : normalized === "warning" ? 5200 : 3800;
const node = el("div", `toast toast-${normalized}`);
node.setAttribute("role", normalized === "error" ? "alert" : "status");
const iconName = normalized === "success" ? "check-circle" : normalized === "error" || normalized === "warning" ? "circle-alert" : "info";
const icon = el("span", "toast-icon");
icon.innerHTML = svgIcon(iconName);
const text = el("span", "toast-message", message);
let actionButton = null;
if (action?.label && typeof action.onClick === "function") {
actionButton = el("button", "toast-action", action.label);
actionButton.type = "button";
}
const countdown = el("span", "toast-countdown");
countdown.setAttribute("aria-hidden", "true");
const close = el("button", "toast-close");
close.type = "button";
close.setAttribute("aria-label", "Dismiss notification");
close.innerHTML = svgIcon("x");
const controls = el("span", "toast-controls");
if (actionButton) controls.append(actionButton);
controls.append(close);
node.append(icon, text, countdown, controls);
const openDialogs = $$("dialog[open]");
let stack = $("#toast-stack");
if (openDialogs.length) {
const dialog = openDialogs[openDialogs.length - 1];
stack = dialog.querySelector(":scope > .modal-toast-stack");
if (!stack) {
stack = el("div", "toast-stack modal-toast-stack");
stack.setAttribute("aria-live", "polite");
dialog.append(stack);
if ([502, 503, 504].includes(response.status) && attempt + 1 < maxAttempts) {
await sleep(250 * (attempt + 1));
continue;
}
const contentType = response.headers.get("content-type") || "";
const payload = contentType.includes("application/json")
? await response.json().catch(() => ({}))
: await response.text().catch(() => "");
if (!response.ok) {
const message = payload?.error?.message || (typeof payload === "string" && payload) || `Request failed (${response.status})`;
const error = new Error(message);
error.status = response.status;
throw error;
}
setConnectivityState(true);
return payload;
} catch (error) {
if (error?.status) throw error;
lastNetworkError = error;
setConnectivityState(false);
if (attempt + 1 < maxAttempts) {
await sleep(350 * (attempt + 1));
continue;
}
}
stack.append(node);
requestAnimationFrame(() => node.classList.add("visible"));
}
const error = new Error(navigator.onLine ? "RustBox server is temporarily unreachable" : "You are offline");
error.cause = lastNetworkError;
error.network = true;
throw error;
}
let remaining = lifetime;
let startedAt = 0;
let timeout = null;
let ticker = null;
let dismissed = false;
const renderCountdown = () => { countdown.textContent = `${Math.max(1, Math.ceil(remaining / 1000))}s`; };
const stopTimers = () => { clearTimeout(timeout); clearInterval(ticker); timeout = null; ticker = null; };
const dismiss = (immediate = false) => {
if (dismissed) return;
dismissed = true;
stopTimers();
if (immediate) return node.remove();
node.classList.remove("visible");
setTimeout(() => node.remove(), 160);
};
const pause = () => {
if (dismissed || !startedAt) return;
remaining = Math.max(0, remaining - (performance.now() - startedAt));
startedAt = 0;
stopTimers();
renderCountdown();
};
const resume = () => {
if (dismissed || remaining <= 0) return dismiss();
stopTimers();
startedAt = performance.now();
renderCountdown();
timeout = setTimeout(() => dismiss(), remaining);
ticker = setInterval(() => {
if (!startedAt) return;
const elapsed = performance.now() - startedAt;
const visibleRemaining = Math.max(0, remaining - elapsed);
countdown.textContent = `${Math.max(1, Math.ceil(visibleRemaining / 1000))}s`;
}, 200);
};
close.addEventListener("click", (event) => {
function uploadRequest(path, formData, onProgress = null) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", apiPath(path), true);
xhr.withCredentials = true;
if (state.accountId) xhr.setRequestHeader("X-Active-Account", state.accountId);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
if (typeof onProgress === "function") {
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable && event.total > 0) onProgress(Math.round((event.loaded / event.total) * 100));
});
}
xhr.addEventListener("error", () => reject(new Error("Upload failed: network error")));
xhr.addEventListener("abort", () => reject(new Error("Upload cancelled")));
xhr.addEventListener("load", () => {
const contentType = xhr.getResponseHeader("content-type") || "";
let payload = xhr.responseText || "";
if (contentType.includes("application/json")) {
try { payload = payload ? JSON.parse(payload) : {}; } catch (_) { payload = {}; }
}
if (xhr.status < 200 || xhr.status >= 300) {
const message = payload?.error?.message || (typeof payload === "string" && payload) || `Upload failed (${xhr.status})`;
const error = new Error(message);
error.status = xhr.status;
reject(error);
return;
}
resolve(payload);
});
xhr.send(formData);
});
}
function toast(message, kind = "info", duration = null, action = null) {
const normalized = ["success", "error", "warning", "info"].includes(kind) ? kind : "info";
const lifetime = Number(duration) > 0 ? Number(duration) : normalized === "error" ? 6500 : normalized === "warning" ? 5200 : 3800;
const node = el("div", `toast toast-${normalized}`);
node.setAttribute("role", normalized === "error" ? "alert" : "status");
const iconName = normalized === "success" ? "check-circle" : normalized === "error" || normalized === "warning" ? "circle-alert" : "info";
const icon = el("span", "toast-icon");
icon.innerHTML = svgIcon(iconName);
const text = el("span", "toast-message", message);
let actionButton = null;
if (action?.label && typeof action.onClick === "function") {
actionButton = el("button", "toast-action", action.label);
actionButton.type = "button";
}
const countdown = el("span", "toast-countdown");
countdown.setAttribute("aria-hidden", "true");
const close = el("button", "toast-close");
close.type = "button";
close.setAttribute("aria-label", "Dismiss notification");
close.innerHTML = svgIcon("x");
const controls = el("span", "toast-controls");
if (actionButton) controls.append(actionButton);
controls.append(close);
node.append(icon, text, countdown, controls);
const openDialogs = $$("dialog[open]");
let stack = $("#toast-stack");
if (openDialogs.length) {
const dialog = openDialogs[openDialogs.length - 1];
stack = dialog.querySelector(":scope > .modal-toast-stack");
if (!stack) {
stack = el("div", "toast-stack modal-toast-stack");
stack.setAttribute("aria-live", "polite");
dialog.append(stack);
}
}
stack.append(node);
requestAnimationFrame(() => node.classList.add("visible"));
let remaining = lifetime;
let startedAt = 0;
let timeout = null;
let ticker = null;
let dismissed = false;
const renderCountdown = () => { countdown.textContent = `${Math.max(1, Math.ceil(remaining / 1000))}s`; };
const stopTimers = () => { clearTimeout(timeout); clearInterval(ticker); timeout = null; ticker = null; };
const dismiss = (immediate = false) => {
if (dismissed) return;
dismissed = true;
stopTimers();
if (immediate) return node.remove();
node.classList.remove("visible");
setTimeout(() => node.remove(), 160);
};
const pause = () => {
if (dismissed || !startedAt) return;
remaining = Math.max(0, remaining - (performance.now() - startedAt));
startedAt = 0;
stopTimers();
renderCountdown();
};
const resume = () => {
if (dismissed || remaining <= 0) return dismiss();
stopTimers();
startedAt = performance.now();
renderCountdown();
timeout = setTimeout(() => dismiss(), remaining);
ticker = setInterval(() => {
if (!startedAt) return;
const elapsed = performance.now() - startedAt;
const visibleRemaining = Math.max(0, remaining - elapsed);
countdown.textContent = `${Math.max(1, Math.ceil(visibleRemaining / 1000))}s`;
}, 200);
};
close.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
dismiss(true);
});
if (actionButton) {
actionButton.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
dismiss(true);
try { await action.onClick(); }
catch (error) { toast(error?.message || "Action failed", "error"); }
});
if (actionButton) {
actionButton.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
dismiss(true);
try { await action.onClick(); }
catch (error) { toast(error?.message || "Action failed", "error"); }
});
}
node.addEventListener("mouseenter", pause);
node.addEventListener("mouseleave", resume);
node.addEventListener("focusin", pause);
node.addEventListener("focusout", (event) => { if (!node.contains(event.relatedTarget)) resume(); });
resume();
}
function progressToast(message) {
const node = el("div", "toast toast-info toast-progress");
node.setAttribute("role", "status");
const icon = el("span", "toast-icon");
icon.innerHTML = svgIcon("paperclip");
const content = el("span", "toast-progress-content");
const text = el("span", "toast-message", message);
const track = el("span", "toast-progress-track");
const bar = el("span", "toast-progress-bar");
track.append(bar);
content.append(text, track);
const value = el("span", "toast-progress-value", "0%");
const close = el("button", "toast-close");
close.type = "button";
close.setAttribute("aria-label", "Dismiss notification");
close.innerHTML = svgIcon("x");
let stack = $("#toast-stack");
const openDialogs = $$("dialog[open]");
if (openDialogs.length) {
const dialog = openDialogs[openDialogs.length - 1];
stack = dialog.querySelector(":scope > .modal-toast-stack");
if (!stack) {
stack = el("div", "toast-stack modal-toast-stack");
stack.setAttribute("aria-live", "polite");
dialog.append(stack);
}
node.addEventListener("mouseenter", pause);
node.addEventListener("mouseleave", resume);
node.addEventListener("focusin", pause);
node.addEventListener("focusout", (event) => { if (!node.contains(event.relatedTarget)) resume(); });
resume();
}
node.append(icon, content, value, close);
stack.append(node);
requestAnimationFrame(() => node.classList.add("visible"));
let dismissed = false;
const dismiss = (delay = 0) => {
if (dismissed) return;
dismissed = true;
setTimeout(() => {
node.classList.remove("visible");
setTimeout(() => node.remove(), 160);
}, Math.max(0, delay));
};
close.addEventListener("click", () => dismiss());
return {
update(percent, label = null) {
const normalized = Math.max(0, Math.min(100, Math.round(Number(percent) || 0)));
bar.style.width = `${normalized}%`;
value.textContent = `${normalized}%`;
if (label) text.textContent = label;
},
complete(label = trText("Upload complete")) {
bar.style.width = "100%";
value.textContent = "100%";
text.textContent = label;
node.classList.remove("toast-info");
node.classList.add("toast-success");
icon.innerHTML = svgIcon("check-circle");
dismiss(1200);
},
fail(label) {
text.textContent = label || trText("Upload failed");
node.classList.remove("toast-info");
node.classList.add("toast-error");
icon.innerHTML = svgIcon("circle-alert");
dismiss(4800);
},
dismiss,
};
}
function appDialog({ title = "RustBox", message = "", input = null, selectOptions = null, confirmLabel = "OK", cancelLabel = "Cancel", danger = false } = {}) {
return new Promise((resolve) => {
const dialog = el("dialog", "dialog app-prompt-dialog");
const form = el("form", "dialog-card app-prompt-card");
form.method = "dialog";
const header = el("header", "dialog-header");
header.append(el("strong", "", title));
const close = el("button", "icon-button");
close.type = "button";
close.setAttribute("aria-label", cancelLabel);
close.innerHTML = svgIcon("x");
header.append(close);
const body = el("div", "app-prompt-body");
if (message) body.append(el("p", "app-prompt-message", message));
let control = null;
if (Array.isArray(selectOptions)) {
control = document.createElement("select");
control.className = "app-prompt-control";
for (const option of selectOptions) control.add(new Option(option.label ?? option.value, option.value));
if (input?.value !== undefined) control.value = String(input.value);
body.append(control);
} else if (input) {
control = input.multiline ? document.createElement("textarea") : document.createElement("input");
control.className = "app-prompt-control";
if (!input.multiline) control.type = input.type || "text";
control.value = input.value ?? "";
if (input.placeholder) control.placeholder = input.placeholder;
if (input.required) control.required = true;
body.append(control);
}
const actions = el("footer", "dialog-actions app-prompt-actions");
const cancel = el("button", "button quiet", cancelLabel);
cancel.type = "button";
const confirm = el("button", danger ? "button danger" : "button primary", confirmLabel);
confirm.type = "submit";
actions.append(cancel, confirm);
form.append(header, body, actions);
dialog.append(form);
document.body.append(dialog);
function appDialog({ title = "RustBox", message = "", input = null, selectOptions = null, confirmLabel = "OK", cancelLabel = "Cancel", danger = false } = {}) {
return new Promise((resolve) => {
const dialog = el("dialog", "dialog app-prompt-dialog");
const form = el("form", "dialog-card app-prompt-card");
form.method = "dialog";
const header = el("header", "dialog-header");
header.append(el("strong", "", title));
const close = el("button", "icon-button");
close.type = "button";
close.setAttribute("aria-label", cancelLabel);
close.innerHTML = svgIcon("x");
header.append(close);
const body = el("div", "app-prompt-body");
if (message) body.append(el("p", "app-prompt-message", message));
let control = null;
if (Array.isArray(selectOptions)) {
control = document.createElement("select");
control.className = "app-prompt-control";
for (const option of selectOptions) control.add(new Option(option.label ?? option.value, option.value));
if (input?.value !== undefined) control.value = String(input.value);
body.append(control);
} else if (input) {
control = input.multiline ? document.createElement("textarea") : document.createElement("input");
control.className = "app-prompt-control";
if (!input.multiline) control.type = input.type || "text";
control.value = input.value ?? "";
if (input.placeholder) control.placeholder = input.placeholder;
if (input.required) control.required = true;
body.append(control);
}
const actions = el("footer", "dialog-actions app-prompt-actions");
const cancel = el("button", "button quiet", cancelLabel);
cancel.type = "button";
const confirm = el("button", danger ? "button danger" : "button primary", confirmLabel);
confirm.type = "submit";
actions.append(cancel, confirm);
form.append(header, body, actions);
dialog.append(form);
document.body.append(dialog);
let settled = false;
const finish = (value) => {
if (settled) return;
settled = true;
resolve(value);
if (dialog.open) dialog.close();
else dialog.remove();
};
close.addEventListener("click", () => finish(null));
cancel.addEventListener("click", () => finish(null));
dialog.addEventListener("cancel", (event) => { event.preventDefault(); finish(null); });
dialog.addEventListener("close", () => { if (!settled) { settled = true; resolve(null); } dialog.remove(); }, { once: true });
form.addEventListener("submit", (event) => {
event.preventDefault();
if (control && !control.reportValidity()) return;
finish(control ? control.value : true);
});
dialog.showModal();
setTimeout(() => (control || confirm).focus(), 20);
let settled = false;
const finish = (value) => {
if (settled) return;
settled = true;
resolve(value);
if (dialog.open) dialog.close();
else dialog.remove();
};
close.addEventListener("click", () => finish(null));
cancel.addEventListener("click", () => finish(null));
dialog.addEventListener("cancel", (event) => { event.preventDefault(); finish(null); });
dialog.addEventListener("close", () => { if (!settled) { settled = true; resolve(null); } dialog.remove(); }, { once: true });
form.addEventListener("submit", (event) => {
event.preventDefault();
if (control && !control.reportValidity()) return;
finish(control ? control.value : true);
});
}
dialog.showModal();
setTimeout(() => (control || confirm).focus(), 20);
});
}
async function appConfirm(message, { title = "Confirm action", confirmLabel = "Continue", danger = false } = {}) {
return (await appDialog({ title, message, confirmLabel, danger })) === true;
}
async function appConfirm(message, { title = "Confirm action", confirmLabel = "Continue", danger = false } = {}) {
return (await appDialog({ title, message, confirmLabel, danger })) === true;
}
function appPrompt(title, { message = "", value = "", placeholder = "", type = "text", confirmLabel = "OK", required = false } = {}) {
return appDialog({ title, message, input: { value, placeholder, type, required }, confirmLabel });
}
function appPrompt(title, { message = "", value = "", placeholder = "", type = "text", confirmLabel = "OK", required = false } = {}) {
return appDialog({ title, message, input: { value, placeholder, type, required }, confirmLabel });
}
function appSelect(title, options, { message = "", value = "", confirmLabel = "Choose" } = {}) {
return appDialog({ title, message, input: { value }, selectOptions: options, confirmLabel });
}
function appSelect(title, options, { message = "", value = "", confirmLabel = "Choose" } = {}) {
return appDialog({ title, message, input: { value }, selectOptions: options, confirmLabel });
}
function setBusy(button, busy) {
if (!button) return;
button.disabled = busy;
button.setAttribute("aria-busy", String(busy));
}
function setBusy(button, busy) {
if (!button) return;
button.disabled = busy;
button.setAttribute("aria-busy", String(busy));
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1153 -1072
View File
File diff suppressed because it is too large. Load diff
+5 -3
View File
@@ -455,7 +455,7 @@ function showContextMenu(x, y, items) {
menu.replaceChildren();
for (const item of items) {
if (item === "separator") { menu.append(el("div", "separator")); continue; }
const button = el("button", item.danger ? "danger" : "", item.label);
const button = el("button", item.danger ? "danger" : "", trText(item.label));
button.type = "button";
button.addEventListener("click", async () => { hideContextMenu(); await item.action(); });
menu.append(button);
@@ -476,7 +476,7 @@ async function ensureMessageOpen(message) {
async function promptFolder(label, exclude = null) {
const options = orderedFolders().map((folder) => folder.name).filter((name) => name !== exclude);
if (!options.length) return null;
return appSelect(label, options.map((name) => ({ value: name, label: name })), { value: options[0], confirmLabel: "Choose folder" });
return appSelect(trText(label), options.map((name) => ({ value: name, label: name })), { value: options[0], confirmLabel: trText("Choose folder") });
}
function openMessageContextMenu(x, y, message) {
@@ -735,6 +735,7 @@ async function showMessageSource() {
const source = await blob.text();
const dialog = document.createElement("dialog");
dialog.className = "dialog";
dialog.dataset.i18nScope = "";
const card = el("div", "dialog-card source-card");
const header = el("header", "dialog-header");
header.append(el("strong", "", "Message source"));
@@ -747,6 +748,7 @@ async function showMessageSource() {
const download = el("button", "button primary", "Download .eml"); download.type = "button"; download.addEventListener("click", downloadEml);
actions.append(copy, download);
card.append(header, pre, actions); dialog.append(card); document.body.append(dialog);
translateScopedUiText(state.prefs.language);
dialog.addEventListener("close", () => dialog.remove(), { once: true });
dialog.showModal();
} catch (error) { toast(error.message, "error"); }
@@ -813,7 +815,7 @@ async function forwardAsAttachment() {
try {
const blob = await fetchEmlBlob(detail);
openCompose("new");
$("#compose-title").textContent = "Forward as attachment";
$("#compose-title").textContent = trText("Forward as attachment");
$("#compose-subject").value = /^fwd:/i.test(detail.subject || "") ? detail.subject : `Fwd: ${detail.subject || ""}`;
const safe = String(detail.subject || `message-${detail.uid}`).replace(/[\\/:*?"<>|]+/g, "_").slice(0, 120) || `message-${detail.uid}`;
const file = new File([blob], `${safe}.eml`, { type: "message/rfc822" });
+27 -10
View File
@@ -2,29 +2,46 @@
async function loadQuota() {
const summary = $("#quota-summary span");
const bar = $("#quota-bar");
if (!summary || !bar) return;
summary.textContent = "Loading quota…";
bar.style.width = "0%";
const sidebar = $("#folder-quota");
const sidebarText = $("#folder-quota-text");
const sidebarBar = $("#folder-quota-bar");
if (!summary && !sidebarText) return;
if (summary) summary.textContent = tr("loading_quota", "Loading quota…");
if (bar) bar.style.width = "0%";
if (sidebarText) sidebarText.textContent = tr("loading", "Loading…");
if (sidebarBar) sidebarBar.style.width = "0%";
const setLevel = (level) => {
for (const box of [$("#quota-summary"), sidebar]) {
box?.classList.toggle("quota-warning", level === "warning");
box?.classList.toggle("quota-danger", level === "danger");
}
};
try {
const quota = await request("/quota", { method: "GET" });
const used = Number(quota.usage_bytes || 0);
const limit = Number(quota.limit_bytes || 0);
if (limit > 0) {
const percent = Math.max(0, Math.min(100, (used / limit) * 100));
summary.textContent = `${formatBytes(used)} of ${formatBytes(limit)} (${percent.toFixed(1)}%)`;
bar.style.width = `${percent}%`;
const quotaBox = $("#quota-summary");
if (summary) summary.textContent = `${formatBytes(used)} ${tr("of", "of")} ${formatBytes(limit)} (${percent.toFixed(1)}%)`;
if (bar) bar.style.width = `${percent}%`;
if (sidebarText) sidebarText.textContent = `${percent.toFixed(0)}% · ${formatBytes(used)} / ${formatBytes(limit)}`;
if (sidebarBar) sidebarBar.style.width = `${percent}%`;
const level = percent >= 95 ? "danger" : percent >= 80 ? "warning" : "normal";
quotaBox?.classList.toggle("quota-warning", level === "warning");
quotaBox?.classList.toggle("quota-danger", level === "danger");
setLevel(level);
if (level !== "normal" && state.quotaWarningLevel !== `${state.accountId}:${level}`) {
state.quotaWarningLevel = `${state.accountId}:${level}`;
toast(level === "danger" ? `Mailbox storage is ${percent.toFixed(0)}% full` : `Mailbox storage is ${percent.toFixed(0)}% used`, level === "danger" ? "error" : "warning");
}
} else {
summary.textContent = `${formatBytes(used)} used · server limit unavailable`;
if (summary) summary.textContent = `${formatBytes(used)} ${tr("used", "used")} · ${tr("server_limit_unavailable", "server limit unavailable")}`;
if (sidebarText) sidebarText.textContent = `${formatBytes(used)} ${tr("used", "used")}`;
setLevel("normal");
}
} catch (error) { summary.textContent = `Quota unavailable: ${error.message}`; }
} catch (error) {
if (summary) summary.textContent = `${tr("quota_unavailable", "Quota unavailable")}: ${error.message}`;
if (sidebarText) sidebarText.textContent = tr("unavailable", "Unavailable");
setLevel("normal");
}
}
function openSettings(tab = "appearance") {
+64 -10
View File
@@ -61,7 +61,41 @@ function bindPaneResizer(selector, preferenceKey, minPx, maxPx) {
});
}
function positionToolbarMenu(menu) {
if (!menu?.open) return;
const popover = menu.querySelector(".toolbar-menu-popover");
const summary = menu.querySelector("summary");
if (!popover || !summary) return;
if (matchMedia("(max-width: 560px)").matches) {
Object.assign(popover.style, { left: "8px", right: "8px", top: "auto", bottom: "8px" });
return;
}
Object.assign(popover.style, { right: "auto", bottom: "auto" });
const rect = summary.getBoundingClientRect();
const width = Math.max(164, popover.offsetWidth || 164);
const height = popover.offsetHeight || 80;
const margin = 8;
let left = Math.min(rect.left, window.innerWidth - width - margin);
left = Math.max(margin, left);
let top = rect.bottom + 5;
if (top + height > window.innerHeight - margin) top = Math.max(margin, rect.top - height - 5);
popover.style.left = `${Math.round(left)}px`;
popover.style.top = `${Math.round(top)}px`;
}
function bindToolbarMenus() {
const menus = $$(".toolbar-menu");
menus.forEach((menu) => menu.addEventListener("toggle", () => {
if (!menu.open) return;
menus.forEach((other) => { if (other !== menu) other.open = false; });
requestAnimationFrame(() => positionToolbarMenu(menu));
}));
window.addEventListener("resize", () => menus.forEach(positionToolbarMenu), { passive: true });
document.addEventListener("scroll", () => menus.forEach(positionToolbarMenu), true);
}
function bindEvents() {
bindToolbarMenus();
bindPaneResizer("#folder-resizer", "folderPaneWidth", 170, 420);
bindPaneResizer("#message-resizer", "messagePaneWidth", 300, 760);
bindRecipientAutocomplete("#compose-to");
@@ -95,20 +129,23 @@ function bindEvents() {
setComposeMode(next, true);
insertIdentitySignature(editor);
scheduleDraftSave();
updateComposeMessageSize();
});
$("#compose-from").addEventListener("change", () => { insertIdentitySignature($("#compose-editor")); updateComposePgpUi(true); });
$("#compose-from").addEventListener("change", () => { insertIdentitySignature($("#compose-editor")); updateComposePgpUi(true); updateComposeMessageSize(); });
["#compose-to", "#compose-cc", "#compose-bcc"].forEach((selector) => $(selector).addEventListener("input", () => {
state.pgpRecipientStatus = [];
updateComposePgpUi();
scheduleDraftSave();
updateComposeMessageSize();
}));
$("#compose-subject").addEventListener("input", scheduleDraftSave);
$("#compose-subject").addEventListener("input", () => { scheduleDraftSave(); updateComposeMessageSize(); });
["#compose-priority", "#compose-mdn", "#compose-pgp-mode", "#compose-pgp-inline", "#compose-pgp-attach-key", "#compose-pgp-attach-recipient-keys"].forEach((selector) => $(selector).addEventListener("change", async () => {
if (selector === "#compose-pgp-attach-recipient-keys" && $(selector).checked) await checkComposePgpRecipients(false, composePgpVisibleRecipients());
updateComposePgpUi();
scheduleDraftSave();
updateComposeMessageSize();
}));
$("#compose-editor").addEventListener("input", () => { if (state.composeSelectedImage && !state.composeSelectedImage.isConnected) setSelectedInlineImage(null); scheduleDraftSave(); scheduleUnusedInlineAttachmentCleanup(); });
$("#compose-editor").addEventListener("input", () => { if (state.composeSelectedImage && !state.composeSelectedImage.isConnected) setSelectedInlineImage(null); scheduleDraftSave(); scheduleUnusedInlineAttachmentCleanup(); updateComposeMessageSize(); });
$("#compose-editor").addEventListener("click", (event) => {
const image = event.target.closest?.("img[data-rustbox-cid]");
setSelectedInlineImage(image || null);
@@ -145,8 +182,16 @@ function bindEvents() {
$("#flag-btn").addEventListener("click", toggleFlagged);
$("#archive-btn").addEventListener("click", archiveCurrent);
$("#spam-btn").addEventListener("click", junkCurrent);
$("#move-select").addEventListener("change", async (event) => { const target = event.target.value; event.target.value = ""; if (target) await moveCurrentTo(target); });
$("#copy-select").addEventListener("change", async (event) => { const target = event.target.value; event.target.value = ""; if (target) await copyCurrentTo(target); });
$("#reader-move-action").addEventListener("click", async () => {
$("#message-transfer-menu").open = false;
const target = await promptFolder("Move to folder", state.currentDetail?.folder || state.folder);
if (target) await moveCurrentTo(target);
});
$("#reader-copy-action").addEventListener("click", async () => {
$("#message-transfer-menu").open = false;
const target = await promptFolder("Copy to folder", state.currentDetail?.folder || state.folder);
if (target) await copyCurrentTo(target);
});
$("#bulk-select-all").addEventListener("change", (event) => { const source = state.searchAllResults || state.messages; for (const message of source) { const key = messageKey(message); event.target.checked ? state.selectedMessages.add(key) : state.selectedMessages.delete(key); } renderMessages(); });
$("#bulk-read").addEventListener("click", () => performBulk("read"));
$("#bulk-unread").addEventListener("click", () => performBulk("unread"));
@@ -221,11 +266,13 @@ function bindEvents() {
$("#rename-folder-btn").addEventListener("click", renameFolder);
$("#delete-folder-btn").addEventListener("click", deleteFolder);
$("#subscribe-folder-btn").addEventListener("click", toggleCurrentFolderSubscription);
$("#headers-btn").addEventListener("click", showHeaders);
$("#source-btn").addEventListener("click", showMessageSource);
$("#headers-btn").addEventListener("click", () => { $("#message-details-menu").open = false; showHeaders(); });
$("#source-btn").addEventListener("click", async () => { $("#message-details-menu").open = false; await showMessageSource(); });
$("#quick-filter-btn").addEventListener("click", () => createFilterFromCurrentMessage("sender"));
$("#reader-from").addEventListener("click", (event) => { const rect = event.currentTarget.getBoundingClientRect(); openCurrentSenderMenu(rect.left, rect.bottom + 4); });
$("#reader-from").addEventListener("keydown", (event) => { if (["Enter", " "].includes(event.key)) { event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); openCurrentSenderMenu(rect.left, rect.bottom + 4); } });
$("#reader-to").addEventListener("click", toggleReaderAddressExpansion);
$("#reader-to").addEventListener("keydown", (event) => { if (["Enter", " "].includes(event.key)) { event.preventDefault(); toggleReaderAddressExpansion(); } });
$("#print-btn").addEventListener("click", printCurrentMessage);
$("#pgp-reader-process").addEventListener("click", processCurrentPgpMessage);
$("#pgp-reader-discover").addEventListener("click", discoverCurrentPgpSigner);
@@ -250,6 +297,7 @@ function bindEvents() {
$$(".settings-tab").forEach((button) => button.addEventListener("click", () => { selectSettingsTab(button.dataset.tab); if (button.dataset.tab === "sieve") loadSieve(); if (button.dataset.tab === "identities") { loadIdentities(); loadOpenPgp(); } if (button.dataset.tab === "openpgp") loadOpenPgp(); if (button.dataset.tab === "notifications") loadNotificationPreferences(); if (button.dataset.tab === "mailbox") loadMailPreferences(); if (button.dataset.tab === "composing") loadResponseTemplates(); if (button.dataset.tab === "diagnostics") loadDiagnostics(); if (button.dataset.tab === "account") { renderAccountSettings(); loadActiveSessions(); } if (button.dataset.tab === "folders") { loadSpecialFolders(); loadQuota(); } }));
$("#pref-theme").addEventListener("change", (event) => savePreference("theme", event.target.value));
$("#pref-interface-skin").addEventListener("change", (event) => savePreference("interfaceSkin", event.target.value));
$("#pref-preset-scale-unlocked").addEventListener("change", (event) => savePreference("presetScaleUnlocked", event.target.checked));
$("#pref-density").addEventListener("change", (event) => savePreference("density", event.target.value));
$("#pref-preview").addEventListener("change", (event) => savePreference("preview", event.target.value));
$("#pref-scale").addEventListener("change", (event) => savePreference("scale", event.target.value));
@@ -396,7 +444,10 @@ function bindEvents() {
$("#sieve-deactivate").addEventListener("click", deactivateSieve);
$("#sieve-delete").addEventListener("click", deleteSieve);
bindEditor();
document.addEventListener("click", (event) => { if (!event.target.closest("#context-menu")) hideContextMenu(); });
document.addEventListener("click", (event) => {
if (!event.target.closest("#context-menu")) hideContextMenu();
if (!event.target.closest(".toolbar-menu")) $$(".toolbar-menu[open]").forEach((menu) => { menu.open = false; });
});
window.addEventListener("blur", hideContextMenu);
document.addEventListener("scroll", hideContextMenu, true);
document.addEventListener("keydown", (event) => {
@@ -438,7 +489,10 @@ function bindEvents() {
else closeReader();
}
});
matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => { if (state.prefs.theme === "system") applyPreferences(); });
matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => {
if (!$("#login-view").classList.contains("hidden")) showLogin();
else if (state.prefs.theme === "system") applyPreferences();
});
window.addEventListener("resize", () => { if (innerWidth > 820) setFolderDrawerOpen(false); });
}
@@ -447,7 +501,7 @@ async function boot() {
bindEvents();
await loadAuthPolicy();
const authenticated = await loadAccounts();
if (authenticated) { await loadPreferences(); await loadMailPreferences(); await loadOpenPgp(); await loadIdentities(); await loadResponseTemplates(); await loadFolders(true); await loadSpecialFolders(); await loadTags(); await loadNotificationPreferences(); await loadTrustedSenders(); startCalendarReminderWatcher(); connectRealtime(); } else { await loadTranslations(state.prefs.language); }
if (authenticated) { await loadPreferences(); await loadMailPreferences(); await loadOpenPgp(); await loadIdentities(); await loadResponseTemplates(); await loadFolders(true); await loadSpecialFolders(); await loadQuota(); await loadTags(); await loadNotificationPreferences(); await loadTrustedSenders(); startCalendarReminderWatcher(); connectRealtime(); } else { await loadTranslations(state.prefs.language); }
}
boot();