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

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-04 20:44:31 +01:00
1 parent 3b9110c8c8
commit beae94e932
42 files changed
+2518 -274

No files matched your search

+11
View File
@@ -1,5 +1,16 @@
# RustBox changelog
### 0.2.23
- Added **Style depth** to Appearance (`skin_depth`, migration `V053`). *Colors only* applies just a predefined style's palette and keeps every layout control editable; *Standard* (default) is the previous behavior — palette plus the style's fixed layout profile; *Immersive* additionally reshapes the interface with the style's own typography, control/button/avatar shapes, floating or flush panes, app bar and search field, sidebar colors, selection markers, card-style rows, primary/compose buttons and backdrop (gradients, glass blur, bevels). Floating-pane geometry applies only to the desktop three-pane layout; phones and tablets keep the drawer layout.
- Added a separate **Style gradients** switch to Appearance (`skin_gradients`, part of migration `V053`, default off). All gradients used by styles — immersive backdrops, app bars, primary buttons, the Synthwave logo and the Roundcube Classic bevelled toolbars/buttons — are applied only when it is on; otherwise each style uses flat colors. Note: Roundcube Classic is therefore flat by default until the switch is enabled.
- Introduced a common interface-style standard. Every predefined style now ships light and dark palettes, a fixed layout profile and a structural token block (`57-skin-tokens.css`, consumed by `58-skin-immersive.css`). All 23 existing styles were adapted to it — for example GitHub gets its dark header and green primary button, Slack its aubergine sidebar, Gmail its pill navigation and light-blue compose button, Outlook its accent app bar and selection bar, Apple Mail its filled selection.
- Added 14 predefined styles: Dracula, Catppuccin, Solarized, Gruvbox, Tokyo Night, Rosé Pine, Everforest, Synthwave, Paper, Terminal, Aqua Glass, Linear inspired, Material You inspired and Retro 95.
- The interface-style catalog is defined once (`display_preferences::INTERFACE_SKINS`) and shared by the user preference and `default_interface_skin` validation. The release validator now checks that every style is present in the catalog, the settings selector, the layout profiles, both palettes and the token table.
- Consolidated migrations V051 and V052 into the fresh schemas; the compatibility baseline is now V052. Databases below V052 must be opened once with 0.2.22 before upgrading.
- Fixed the fresh MySQL schema: `mail_preferences.message_loading_mode` used a literal `DEFAULT` on a `TEXT` column, which MySQL rejects; it now uses an expression default.
- Bumped the application/source release to 0.2.23.
### 0.2.22
- Added message snooze. A new `snoozed_messages` table (migration `V052`) records a wake time for a message; snoozing it (via the new "Snooze…" context-menu entry or the `b` shortcut while a message is open) hides it client-side from its folder's current view until that time, using `GET /folders/{name}/snoozed-uids` to know which UIDs are currently asleep. `POST /messages/{folder}/{uid}/snooze` and `DELETE /messages/{folder}/{uid}/snooze` set/clear a snooze; `GET /messages/snoozed` lists everything currently sleeping, shown in the new "View snoozed messages…" dialog (`Shift+B`). This is purely a local cache: it has no IMAP side effects and does not change server-side folder pagination/count queries.
Generated
+1 -1
View File
@@ -3506,7 +3506,7 @@ dependencies = [
[[package]]
name = "rustbox"
version = "0.2.22"
version = "0.2.23"
dependencies = [
"aes-gcm",
"async-imap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustbox"
version = "0.2.22"
version = "0.2.23"
edition = "2024"
rust-version = "1.99.1"
default-run = "rustbox"
+1 -1
View File
@@ -1,6 +1,6 @@
# RustBox attribution / notice
RustBox 0.2.22 is derived from the Oxi webmail project:
RustBox 0.2.23 is derived from the Oxi webmail project:
- Upstream: `c0h1b4/oxi`
- Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d`
+3 -3
View File
@@ -1,4 +1,4 @@
# RustBox 0.2.22
# RustBox 0.2.23
RustBox is a webmail derived from `c0h1b4/oxi`. The consumed Oxi revision is recorded in `UPSTREAM_BASE`.
@@ -67,7 +67,7 @@ Edit `config.dev.toml` before the first run:
```toml
default_theme = "system" # system | dark | light
default_interface_skin = "rustbox" # rustbox | gmail | proton | zoho | outlook | fastmail | apple-mail | yahoo | hey | nord | github | gitlab | discord | slack | reddit | notion | ...
default_interface_skin = "rustbox" # rustbox | gmail | outlook | apple-mail | github | slack | dracula | catppuccin | tokyo-night | paper | terminal | aqua | material | retro | ... (full list in config.example.toml)
log_level = "info" # RustBox app level; dependencies default to WARN. RUST_LOG can override the full filter.
# Recommended dev filter: warn,rustbox=debug,tower_http=info
# Avoid a global RUST_LOG=debug/info unless diagnosing a dependency: third-party
@@ -183,7 +183,7 @@ rustbox search reindex
Ready Docker examples are in `docker/`: standalone SQLite, PostgreSQL + SILO (S3-compatible) + OpenSearch, and MySQL + NFS + OpenSearch.
All executable application SQL is centralized by dialect under `src/queries/{sqlite,mysql,pgsql}/queries.rs`. Migrations V047-V050 are consolidated into the V050 compatibility baseline; databases older than V050 must first be upgraded with a release that can reach that baseline. RustBox 0.2.16 adds V051 for adaptive HTML-mail colors and mailbox loading mode; fresh schemas already contain those columns, while existing V050 tenants migrate automatically to V051. V052 is the next migration number. See `migrations/README.md`.
All executable application SQL is centralized by dialect under `src/queries/{sqlite,mysql,pgsql}/queries.rs`. Migrations V047-V052 are consolidated into the V052 compatibility baseline; databases older than V052 must first be upgraded with a release that can reach that baseline (0.2.22). RustBox 0.2.23 adds V053 for the appearance `skin_depth` and `skin_gradients` settings; fresh schemas already contain the column, while existing V052 tenants migrate automatically. V054 is the next migration number. See `migrations/README.md`.
## Development
+16 -8
View File
@@ -1,4 +1,13 @@
RustBox 0.2.22 release manifest
RustBox 0.2.23 release manifest
0.2.23 release changes:
- adds a "Style depth" appearance setting (`skin_depth`, migration V053): `colors` applies only a predefined style's palette, `standard` (default, previous behavior) adds its fixed layout profile, `immersive` additionally applies its structural design tokens — typography, control/button/avatar shapes, floating or flush panes, app-bar and search treatment, selection markers, row cards and backdrop
- adds a separate "Style gradients" switch (`skin_gradients`, default off, same V053 migration): every gradient used by a style — backdrops, app bars, primary buttons, Roundcube Classic bevels — is applied only when it is on; otherwise each style uses its flat fallback colors
- introduces the interface-style standard: every predefined style now ships light + dark palettes, a fixed layout profile and a structural token block (`57-skin-tokens.css`, consumed by `58-skin-immersive.css`); all 23 existing styles were adapted to it
- adds 14 predefined styles: Dracula, Catppuccin, Solarized, Gruvbox, Tokyo Night, Rosé Pine, Everforest, Synthwave, Paper, Terminal, Aqua Glass, Linear inspired, Material You inspired and Retro 95
- the interface-style catalog is defined once in Rust (`INTERFACE_SKINS`) and shared by preference and `default_interface_skin` validation; the release validator enforces catalog/selector/profile/palette/token parity
- consolidates V051 and V052 into the fresh schemas; the compatibility baseline is now V052
- fixes the fresh MySQL schema: `mail_preferences.message_loading_mode` now uses an expression default (MySQL rejects literal defaults on TEXT)
0.2.22 release changes:
- adds message snooze: snoozing a message hides it client-side from its folder's current view until the chosen wake time, without touching server-side folder pagination/count SQL; `snoozed_messages` is a local-only cache table with no IMAP side effects, so a snoozed message is unaffected on any other client
@@ -71,11 +80,10 @@ Search:
Database/schema:
- migrations/{sqlite,mysql,pgsql}/schema.sql contain the current fresh schema
- compatibility baseline is V050; former V047-V050 changes are consolidated into `schema.sql` and their standalone migrations are removed
- databases older than V050 must be upgraded with a V050-capable RustBox release before using this build
- V051 adds `display_preferences.adaptive_email_colors` and `mail_preferences.message_loading_mode` to existing V050 tenants
- V052 adds `snoozed_messages` (local-only cache state, no IMAP side effects) to existing V051 tenants
- next migration number: V053
- compatibility baseline is V052; former V047-V052 changes are consolidated into `schema.sql` and their standalone migrations are removed
- databases older than V052 must be upgraded with a V052-capable RustBox release (0.2.22) before using this build
- V053 adds `display_preferences.skin_depth` (default `standard`) and `display_preferences.skin_gradients` (default off) to existing V052 tenants
- next migration number: V054
Docker examples:
- docker/compose-single-sqlite.yml
@@ -85,14 +93,14 @@ Docker examples:
Operations/docs:
- standalone `scripts/migrate_roundcube.py` imports Roundcube contacts/identities using RustBox config.toml and supports SQLite/MySQL/PostgreSQL targets
- README.md documents 0.2.22 configuration
- README.md documents 0.2.23 configuration
- docker/README.md documents Compose variants
- docs/STORAGE_SEARCH_OPERATIONS.md documents failure modes, shared-storage requirements and migration behavior
- TODO.md leaves live SILO/MinIO/AWS/Ceph and real multi-node acceptance validation explicitly pending
Build/release metadata:
- `scripts/package-release.sh` runs the full Rust release gate before creating ZIP/SHA256 artifacts
- Cargo.toml version = 0.2.22
- Cargo.toml version = 0.2.23
- rust-toolchain.toml pins Rust 1.99.1
- Cargo.lock is retained for reproducible application builds; the release gate generates it only when missing
- CI and scripts/build-release.sh use the retained Cargo.lock (generating it only if missing), then run locked tests/Clippy/release build
+14 -6
View File
@@ -1,4 +1,11 @@
RustBox 0.2.22 - final source validation
RustBox 0.2.23 - final source validation
0.2.23 scope:
- adds Appearance "Style depth" (`skin_depth`, V053): colors only / standard (default) / immersive
- adds a separate "Style gradients" switch (`skin_gradients`, V053, default off); every stylesheet gradient is gated by it
- interface-style standard: every predefined style ships light+dark palettes, a fixed layout profile and structural design tokens; 23 existing styles adapted, 14 new styles added (Dracula, Catppuccin, Solarized, Gruvbox, Tokyo Night, Rosé Pine, Everforest, Synthwave, Paper, Terminal, Aqua Glass, Linear, Material You, Retro 95)
- V051/V052 consolidated into the fresh schemas; compatibility baseline V052
- fresh MySQL schema: `message_loading_mode` uses an expression default
0.2.22 scope:
- adds message snooze (`snoozed_messages`, V052): hides a message client-side from its current folder view until a chosen wake time; purely client-side hiding, no server-side pagination/count SQL changes, no IMAP side effects
@@ -45,17 +52,18 @@ RustBox 0.2.22 - final source validation
- PDF attachment preview uses the authenticated inline URL for Safari/iPhone compatibility
- Composing shows only compact attachment/message size-limit values on one line
- 0.2.9 realtime notification gating, browser shortcut preservation and 300-message defaults remain intact
- V047-V050 are consolidated into compatibility baseline V050; V051 migrates existing tenants with adaptive HTML-mail colors and mailbox loading mode; V052 adds `snoozed_messages` to existing V051 tenants; V053 is next
- version metadata identifies 0.2.22
- V047-V052 are consolidated into compatibility baseline V052; V053 adds `skin_depth` and `skin_gradients` to existing V052 tenants; V054 is next
- version metadata identifies 0.2.23
Completed in this environment:
- offline release validator: scripts/validate-release-static.py (424/424 checks passed)
- offline release validator: scripts/validate-release-static.py (432/432 checks passed)
- browser JavaScript assembled in manifest order passes `node --check`
- all JSON files parse successfully
- Python helper scripts compile successfully
- shell scripts pass `bash -n`
- release metadata consistently identifies 0.2.22
- V050 baseline consolidation plus the V051 adaptive-colors/mailbox-loading migration are validated offline
- release metadata consistently identifies 0.2.23
- V052 baseline consolidation plus the V053 skin-depth migration are validated offline
- all 37 interface styles rendered in light and dark at the immersive depth against a mocked API (Playwright), with no page errors
Not executable in this packaging environment:
+7 -2
View File
@@ -11,8 +11,13 @@ cookie_secure = "auto"
# Global light/dark theme: system | dark | light
default_theme = "system"
# Global interface style used by the login screen and by accounts set to
# "Global default". Presets include gmail, proton, zoho, outlook, fastmail,
# apple-mail, yahoo, hey, nord, github, gitlab, discord, slack, reddit, notion, minimal and Roundcube variants.
# "Global default". Each user can additionally choose a style depth (colors only,
# standard or immersive) in Appearance. Available styles:
# rustbox, roundcube-classic, roundcube-larry, roundcube-elastic, clean, gmail,
# proton, zoho, outlook, minimal, graphite, high-contrast, fastmail, apple-mail,
# yahoo, hey, nord, github, gitlab, discord, slack, reddit, notion, dracula,
# catppuccin, solarized, gruvbox, tokyo-night, rose-pine, everforest, synthwave,
# paper, terminal, aqua, linear, material, retro
default_interface_skin = "gmail"
data_dir = "/var/lib/rustbox"
log_dir = "/var/log/rustbox"
+12 -3
View File
@@ -1,7 +1,16 @@
# RustBox 0.2.22 - implementation status
# RustBox 0.2.23 - implementation status
- Draft saves are realtime-silent: Drafts folder/list updates continue, but saving a draft does not surface as a new-mail notification.
This file tracks the `0.2.22` source snapshot. Storage/search scope and outstanding acceptance work are tracked in `TODO.md`. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly.
This file tracks the `0.2.23` source snapshot. Storage/search scope and outstanding acceptance work are tracked in `TODO.md`. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly.
## 0.2.23 interface styles
- Adds Appearance → **Style depth** (`display_preferences.skin_depth`, migration V053): `colors` applies only a predefined style's palette and leaves layout controls editable; `standard` (default) keeps the previous palette + fixed layout behavior; `immersive` additionally applies the style's structural tokens.
- Adds Appearance → **Style gradients** (`display_preferences.skin_gradients`, same V053 migration, default off). Every gradient a style uses (immersive backdrops, app bars, primary buttons, the Synthwave logo, Roundcube Classic bevels in any depth) is scoped to `html[data-skin-gradients="true"]`; with the switch off the style uses flat fallback colors. The release validator rejects ungated gradients.
- Interface-style standard: each predefined style has light and dark palettes (`55-roundcube-skins.css`, `56-interface-styles.css`), a fixed layout profile (`interfaceStyleProfiles`) and a structural token block in `57-skin-tokens.css` (fonts, heading treatment, control/button/compose/avatar radii, floating vs. flush panes, pane borders/shadows/glass, app bar and search field, sidebar colors, selection background/marker, row cards, primary/compose buttons, focus ring, bevels). `58-skin-immersive.css` consumes the tokens only at the immersive depth; floating-pane geometry is limited to the desktop three-pane layout (>820px).
- All 23 existing styles were adapted to the standard; 14 styles were added: Dracula, Catppuccin, Solarized, Gruvbox, Tokyo Night, Rosé Pine, Everforest, Synthwave, Paper, Terminal, Aqua Glass, Linear inspired, Material You inspired, Retro 95.
- The style catalog lives in `display_preferences::INTERFACE_SKINS` and is shared by preference and `default_interface_skin` validation; `scripts/validate-release-static.py` checks catalog/selector/profile/palette/token parity.
- V051 and V052 are consolidated into the fresh schemas (baseline V052). The fresh MySQL schema now uses an expression default for `mail_preferences.message_loading_mode`.
## 0.2.22 snooze and undo send
@@ -50,7 +59,7 @@ This file tracks the `0.2.22` source snapshot. Storage/search scope and outstand
- Initial synchronization remains silent for new-mail notifications; bulk operations retain one summary toast; browser/system modifier shortcuts remain preserved.
- Mailbox pagination is enabled by default at 300 messages per page; the API/preferences accept page sizes up to 300, with progressive and load-all alternatives.
- Blob deletion uses a durable SQL outbox for filesystem/shared POSIX/NFS and S3 backends; attachment/draft metadata deletion is transactionally paired with cleanup enqueue and failed deletes retry in the background.
- V047-V050 are consolidated into the V050 compatibility baseline. RustBox 0.2.16 uses V051 to add adaptive HTML-mail colors and mailbox loading mode to existing V050 tenants; V052 is next.
- V047-V052 are consolidated into the V052 compatibility baseline (0.2.23). V053 adds `skin_depth` and `skin_gradients` to existing V052 tenants; V054 is next.
## 0.2.8 browser security headers
+17 -12
View File
@@ -8,28 +8,31 @@ RustBox keeps one complete fresh-install schema per supported SQL backend:
## Compatibility baseline
The consolidated compatibility baseline is **V050**. Historical migrations V047-V050 are already represented by the complete `schema.sql` files and are intentionally not shipped as standalone runtime migrations in this source tree.
The consolidated compatibility baseline is **V052** (RustBox 0.2.23). Historical migrations up to and including V052 are already represented by the complete `schema.sql` files and are intentionally not shipped as standalone runtime migrations in this source tree:
Databases older than V050 must first be upgraded with a RustBox release that can reach V050. RustBox 0.2.16 accepts V050 databases and migrates them forward normally.
- V047-V050 — attachment/search-outbox changes, folder counter generation, blob deletion outbox, compact-row and signature preferences (consolidated in 0.2.16);
- V051 — `display_preferences.adaptive_email_colors` (default `0`) and `mail_preferences.message_loading_mode` (default `pagination`);
- V052 — the `snoozed_messages` table used by Snooze (local cache state only, no IMAP side effects, no foreign keys).
## V051 — RustBox 0.2.16
Databases older than V052 must first be upgraded with a RustBox release that can reach V052. RustBox 0.2.22 accepts V050 and V051 databases and migrates them to V052, so a tenant that has not logged in since an older release should be opened once with 0.2.22 before upgrading. Tenants below the baseline are refused with an explicit error instead of being partially migrated.
V051 adds the preferences introduced in 0.2.16:
The MySQL fresh schema now declares `mail_preferences.message_loading_mode` with an expression default (`DEFAULT ('pagination')`); MySQL rejects literal defaults on `TEXT` columns.
- `display_preferences.adaptive_email_colors`, default `0` / disabled;
- `mail_preferences.message_loading_mode`, default `pagination`.
## V053 — RustBox 0.2.23
`mail_preferences.messages_per_page` remains `300` by default. Supported message loading modes are `pagination`, `progressive` and `all`.
V053 adds two appearance columns to `display_preferences`.
Fresh installations already contain the V051 columns in `schema.sql` and are registered at the latest schema version. Existing V050 tenants execute `V051__adaptive_email_colors_and_message_loading_mode.sql` before application queries use the new columns.
`skin_depth`, default `standard`, controls how far a predefined interface style may reshape the interface:
## V052 — RustBox 0.2.22
- `colors` — only the style's palette is applied; layout, shapes and typography come from your own appearance settings;
- `standard` — palette plus the style's fixed layout preferences (the behavior of earlier releases);
- `immersive` — additionally applies the style's structural design tokens: typography, control and avatar shapes, floating panes, app-bar treatment, selection markers and backdrop.
V052 adds the `snoozed_messages` table used by the Snooze feature introduced in 0.2.22: a message can be hidden from its folder listing in the client until a chosen wake time, without being moved on the real IMAP server. The table is local cache state only (no IMAP side effects) and has no foreign keys, matching `message_tags`' existing exemption from forward-referencing foreign keys in the MySQL/PostgreSQL schemas.
`skin_gradients`, default `0` / off, is a separate switch for every gradient a style uses (backdrops, app bars, buttons, Roundcube Classic bevels). When off, each style falls back to flat colors defined next to its gradient.
Fresh installations already contain `snoozed_messages` in `schema.sql` and are registered at the latest schema version. Existing V051 tenants execute `V052__snooze.sql`.
Fresh installations already contain both columns in `schema.sql` and are registered at the latest schema version. Existing V052 tenants execute `V053__skin_depth_and_gradients.sql`.
The next migration number is **V053**.
The next migration number is **V054**.
## Migration rules
@@ -39,3 +42,5 @@ Any future schema-changing release must:
2. add the same logical numbered migration for SQLite, MySQL and PostgreSQL;
3. register that migration in `src/db/pool.rs`;
4. extend the offline migration tests in `scripts/validate-release-static.py`.
On MySQL, `TEXT` columns must use expression defaults such as `DEFAULT ('value')`.
@@ -1,5 +0,0 @@
ALTER TABLE {{p}}display_preferences
ADD COLUMN adaptive_email_colors BIGINT NOT NULL DEFAULT 0;
ALTER TABLE {{p}}mail_preferences
ADD COLUMN message_loading_mode TEXT NOT NULL DEFAULT 'pagination';
-9
View File
@@ -1,9 +0,0 @@
CREATE TABLE {{p}}snoozed_messages (
id VARCHAR(64) PRIMARY KEY,
message_uid BIGINT NOT NULL,
message_folder VARCHAR(512) NOT NULL,
wake_at BIGINT NOT NULL,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
UNIQUE KEY idx_snoozed_messages_unique (message_uid, message_folder),
INDEX idx_snoozed_messages_wake (wake_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,3 @@
ALTER TABLE {{p}}display_preferences
ADD COLUMN skin_depth TEXT NOT NULL DEFAULT ('standard'),
ADD COLUMN skin_gradients BIGINT NOT NULL DEFAULT 0;
+6 -4
View File
@@ -1,13 +1,13 @@
-- RustBox 0.2.0 MySQL schema.
-- One physical database contains global sessions plus isolated per-account tables.
-- Application tables are materialized by replacing {{p}} with a collision-checked prefix.
-- Squashed compatibility baseline: V050; current schema: V052.
-- Squashed compatibility baseline: V052; current schema: V053.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
user_hash VARCHAR(128) PRIMARY KEY,
table_prefix VARCHAR(64) NOT NULL UNIQUE,
schema_version BIGINT NOT NULL DEFAULT 50,
schema_version BIGINT NOT NULL DEFAULT 52,
created_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR)),
updated_at VARCHAR(64) NOT NULL DEFAULT (CAST(CURRENT_TIMESTAMP AS CHAR))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -177,7 +177,9 @@ CREATE TABLE {{p}}display_preferences (
spellcheck_enabled BIGINT NOT NULL DEFAULT 1,
interface_skin TEXT NOT NULL DEFAULT ('rustbox'),
preset_scale_unlocked BIGINT NOT NULL DEFAULT 0,
adaptive_email_colors BIGINT NOT NULL DEFAULT 0
adaptive_email_colors BIGINT NOT NULL DEFAULT 0,
skin_depth TEXT NOT NULL DEFAULT ('standard'),
skin_gradients BIGINT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE {{p}}draft_attachments (
@@ -297,7 +299,7 @@ CREATE TABLE {{p}}known_addresses (
CREATE TABLE {{p}}mail_preferences (
id BIGINT PRIMARY KEY,
messages_per_page BIGINT NOT NULL DEFAULT 300,
message_loading_mode TEXT NOT NULL DEFAULT 'pagination',
message_loading_mode TEXT NOT NULL DEFAULT ('pagination'),
mark_read_delay_ms BIGINT NOT NULL DEFAULT 0,
remote_images TEXT NOT NULL DEFAULT ('ask'),
preferred_body TEXT NOT NULL DEFAULT ('auto'),
@@ -1,5 +0,0 @@
ALTER TABLE {{p}}display_preferences
ADD COLUMN adaptive_email_colors BIGINT NOT NULL DEFAULT 0;
ALTER TABLE {{p}}mail_preferences
ADD COLUMN message_loading_mode TEXT NOT NULL DEFAULT 'pagination';
-10
View File
@@ -1,10 +0,0 @@
CREATE TABLE {{p}}snoozed_messages (
id TEXT PRIMARY KEY,
message_uid BIGINT NOT NULL,
message_folder TEXT NOT NULL,
wake_at BIGINT NOT NULL,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
UNIQUE (message_uid, message_folder)
);
CREATE INDEX {{p}}idx_snoozed_messages_wake ON {{p}}snoozed_messages(wake_at);
@@ -0,0 +1,3 @@
ALTER TABLE {{p}}display_preferences
ADD COLUMN skin_depth TEXT NOT NULL DEFAULT 'standard',
ADD COLUMN skin_gradients BIGINT NOT NULL DEFAULT 0;
+5 -3
View File
@@ -1,13 +1,13 @@
-- RustBox 0.2.0 PostgreSQL schema.
-- One physical database contains global sessions plus isolated per-account tables.
-- Application tables are materialized by replacing {{p}} with a collision-checked prefix.
-- Squashed compatibility baseline: V050; current schema: V052.
-- Squashed compatibility baseline: V052; current schema: V053.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
user_hash VARCHAR(128) PRIMARY KEY,
table_prefix VARCHAR(64) NOT NULL UNIQUE,
schema_version BIGINT NOT NULL DEFAULT 50,
schema_version BIGINT NOT NULL DEFAULT 52,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text)
);
@@ -177,7 +177,9 @@ CREATE TABLE {{p}}display_preferences (
spellcheck_enabled BIGINT NOT NULL DEFAULT 1,
interface_skin TEXT NOT NULL DEFAULT 'rustbox',
preset_scale_unlocked BIGINT NOT NULL DEFAULT 0,
adaptive_email_colors BIGINT NOT NULL DEFAULT 0
adaptive_email_colors BIGINT NOT NULL DEFAULT 0,
skin_depth TEXT NOT NULL DEFAULT 'standard',
skin_gradients BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE {{p}}draft_attachments (
@@ -1,5 +0,0 @@
ALTER TABLE {{p}}display_preferences
ADD COLUMN adaptive_email_colors INTEGER NOT NULL DEFAULT 0;
ALTER TABLE {{p}}mail_preferences
ADD COLUMN message_loading_mode TEXT NOT NULL DEFAULT 'pagination';
-10
View File
@@ -1,10 +0,0 @@
CREATE TABLE {{p}}snoozed_messages (
id TEXT PRIMARY KEY,
message_uid INTEGER NOT NULL,
message_folder TEXT NOT NULL,
wake_at INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (message_uid, message_folder)
);
CREATE INDEX {{p}}idx_snoozed_messages_wake ON {{p}}snoozed_messages(wake_at);
@@ -0,0 +1,5 @@
ALTER TABLE {{p}}display_preferences
ADD COLUMN skin_depth TEXT NOT NULL DEFAULT 'standard';
ALTER TABLE {{p}}display_preferences
ADD COLUMN skin_gradients INTEGER NOT NULL DEFAULT 0;
+3 -3
View File
@@ -1,13 +1,13 @@
-- RustBox 0.2.0 SQLite schema.
-- One physical database contains global sessions plus isolated per-account tables.
-- Application tables are materialized from the tenant section by replacing {{p}}.
-- Squashed compatibility baseline: V050; current schema: V052.
-- Squashed compatibility baseline: V052; current schema: V053.
-- RUSTBOX:GLOBAL-BEGIN
CREATE TABLE IF NOT EXISTS rustbox_accounts (
user_hash TEXT PRIMARY KEY NOT NULL,
table_prefix TEXT NOT NULL UNIQUE,
schema_version INTEGER NOT NULL DEFAULT 50,
schema_version INTEGER NOT NULL DEFAULT 52,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -122,7 +122,7 @@ CREATE TABLE {{p}}display_preferences (
theme TEXT NOT NULL DEFAULT 'system',
language TEXT NOT NULL DEFAULT 'en',
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
compose_format TEXT NOT NULL DEFAULT 'html', deep_index BOOLEAN NOT NULL DEFAULT 0, animation_mode TEXT NULL, preview_pane TEXT NOT NULL DEFAULT 'right', ui_scale TEXT NOT NULL DEFAULT 'normal', color_preset TEXT NOT NULL DEFAULT 'sage', accent_color TEXT NOT NULL DEFAULT '#6f9c86', font_family TEXT NOT NULL DEFAULT 'system', font_size INTEGER NOT NULL DEFAULT 13, row_height INTEGER NOT NULL DEFAULT 58, folder_pane_width INTEGER NOT NULL DEFAULT 228, message_pane_width INTEGER NOT NULL DEFAULT 420, border_radius INTEGER NOT NULL DEFAULT 4, contrast TEXT NOT NULL DEFAULT 'normal', show_avatars INTEGER NOT NULL DEFAULT 1, show_attachment_icons INTEGER NOT NULL DEFAULT 1, show_flags INTEGER NOT NULL DEFAULT 1, reduce_motion INTEGER NOT NULL DEFAULT 0, date_format TEXT NOT NULL DEFAULT 'locale', time_format TEXT NOT NULL DEFAULT 'locale', background_color TEXT NOT NULL DEFAULT '#121513', panel_color TEXT NOT NULL DEFAULT '#181c19', text_color TEXT NOT NULL DEFAULT '#edf0ec', compose_autosave_ms INTEGER NOT NULL DEFAULT 3000, default_priority TEXT NOT NULL DEFAULT 'normal', request_mdn_default INTEGER NOT NULL DEFAULT 0, attachment_reminder INTEGER NOT NULL DEFAULT 1, icon_size INTEGER NOT NULL DEFAULT 18, toolbar_density TEXT NOT NULL DEFAULT 'normal', sidebar_item_height INTEGER NOT NULL DEFAULT 29, reader_max_width INTEGER NOT NULL DEFAULT 0, composer_width INTEGER NOT NULL DEFAULT 900, selection_color TEXT NOT NULL DEFAULT '#28352e', unread_font_weight INTEGER NOT NULL DEFAULT 730, unread_badge_style TEXT NOT NULL DEFAULT 'dot', compact_headers INTEGER NOT NULL DEFAULT 0, show_borders INTEGER NOT NULL DEFAULT 1, scrollbar_density TEXT NOT NULL DEFAULT 'thin', monochrome INTEGER NOT NULL DEFAULT 0, folder_background TEXT NOT NULL DEFAULT '', message_background TEXT NOT NULL DEFAULT '', reader_background TEXT NOT NULL DEFAULT '', compose_font_family TEXT NOT NULL DEFAULT 'system', reply_position TEXT NOT NULL DEFAULT 'above', quote_style TEXT NOT NULL DEFAULT 'standard', forward_mode TEXT NOT NULL DEFAULT 'inline', spellcheck_enabled INTEGER NOT NULL DEFAULT 1, interface_skin TEXT NOT NULL DEFAULT 'rustbox', preset_scale_unlocked INTEGER NOT NULL DEFAULT 0, adaptive_email_colors INTEGER NOT NULL DEFAULT 0);
compose_format TEXT NOT NULL DEFAULT 'html', deep_index BOOLEAN NOT NULL DEFAULT 0, animation_mode TEXT NULL, preview_pane TEXT NOT NULL DEFAULT 'right', ui_scale TEXT NOT NULL DEFAULT 'normal', color_preset TEXT NOT NULL DEFAULT 'sage', accent_color TEXT NOT NULL DEFAULT '#6f9c86', font_family TEXT NOT NULL DEFAULT 'system', font_size INTEGER NOT NULL DEFAULT 13, row_height INTEGER NOT NULL DEFAULT 58, folder_pane_width INTEGER NOT NULL DEFAULT 228, message_pane_width INTEGER NOT NULL DEFAULT 420, border_radius INTEGER NOT NULL DEFAULT 4, contrast TEXT NOT NULL DEFAULT 'normal', show_avatars INTEGER NOT NULL DEFAULT 1, show_attachment_icons INTEGER NOT NULL DEFAULT 1, show_flags INTEGER NOT NULL DEFAULT 1, reduce_motion INTEGER NOT NULL DEFAULT 0, date_format TEXT NOT NULL DEFAULT 'locale', time_format TEXT NOT NULL DEFAULT 'locale', background_color TEXT NOT NULL DEFAULT '#121513', panel_color TEXT NOT NULL DEFAULT '#181c19', text_color TEXT NOT NULL DEFAULT '#edf0ec', compose_autosave_ms INTEGER NOT NULL DEFAULT 3000, default_priority TEXT NOT NULL DEFAULT 'normal', request_mdn_default INTEGER NOT NULL DEFAULT 0, attachment_reminder INTEGER NOT NULL DEFAULT 1, icon_size INTEGER NOT NULL DEFAULT 18, toolbar_density TEXT NOT NULL DEFAULT 'normal', sidebar_item_height INTEGER NOT NULL DEFAULT 29, reader_max_width INTEGER NOT NULL DEFAULT 0, composer_width INTEGER NOT NULL DEFAULT 900, selection_color TEXT NOT NULL DEFAULT '#28352e', unread_font_weight INTEGER NOT NULL DEFAULT 730, unread_badge_style TEXT NOT NULL DEFAULT 'dot', compact_headers INTEGER NOT NULL DEFAULT 0, show_borders INTEGER NOT NULL DEFAULT 1, scrollbar_density TEXT NOT NULL DEFAULT 'thin', monochrome INTEGER NOT NULL DEFAULT 0, folder_background TEXT NOT NULL DEFAULT '', message_background TEXT NOT NULL DEFAULT '', reader_background TEXT NOT NULL DEFAULT '', compose_font_family TEXT NOT NULL DEFAULT 'system', reply_position TEXT NOT NULL DEFAULT 'above', quote_style TEXT NOT NULL DEFAULT 'standard', forward_mode TEXT NOT NULL DEFAULT 'inline', spellcheck_enabled INTEGER NOT NULL DEFAULT 1, interface_skin TEXT NOT NULL DEFAULT 'rustbox', preset_scale_unlocked INTEGER NOT NULL DEFAULT 0, adaptive_email_colors INTEGER NOT NULL DEFAULT 0, skin_depth TEXT NOT NULL DEFAULT 'standard', skin_gradients INTEGER NOT NULL DEFAULT 0);
CREATE TABLE {{p}}draft_attachments (
id TEXT PRIMARY KEY,
+4 -4
View File
@@ -35,10 +35,10 @@ log_level = "warn"
# system | dark | light
default_theme = "system"
# Built-in examples:
# gmail, proton, zoho, outlook, fastmail, apple-mail, yahoo, hey,
# nord, github, gitlab, discord, slack, reddit, notion, minimal,
# and Roundcube variants.
# Built-in styles (full list in config.example.toml), e.g.:
# gmail, outlook, apple-mail, fastmail, github, slack, notion, nord,
# dracula, catppuccin, tokyo-night, rose-pine, paper, terminal, aqua,
# material, retro and Roundcube variants.
default_interface_skin = "apple-mail"
# Optional product features. Disabled features are removed from the API/UI.
Executable → Regular
+1 -1
View File
@@ -453,7 +453,7 @@ def sqlite_tenant_schema() -> tuple[str, int]:
raise ValueError(f"invalid RustBox tenant schema markers in {schema_path}")
tenant_sql = schema.split(begin, 1)[1].split(end, 1)[0]
versions = [50]
versions = [52]
for path in root.glob("V[0-9][0-9][0-9]__*.sql"):
try:
versions.append(int(path.name[1:4]))
+72
View File
@@ -941,6 +941,78 @@ try:
except Exception as exc:
errors.append(f"shell syntax validation failed: {exc}")
# Interface-style standard (0.2.23): every predefined style must be listed in
# the Rust catalog, the settings selector, the JS layout profiles, both palettes
# and the structural token table, so a style can never be half-defined.
skin_catalog_match = re.search(r"pub const INTERFACE_SKINS: &\[&str\] = &\[(.*?)\];", display_prefs_rs, re.S)
skin_catalog = re.findall(r'"([a-z0-9-]+)"', skin_catalog_match.group(1)) if skin_catalog_match else []
require(len(skin_catalog) >= 30 and len(skin_catalog) == len(set(skin_catalog)) and "default" not in skin_catalog, "Rust interface-style catalog is complete and duplicate-free")
require("crate::db::display_preferences::INTERFACE_SKINS" in read("src/config.rs"), "server default interface style is validated against the shared Rust catalog")
skin_select = re.search(r'<select id="pref-interface-skin">(.*?)</select>', index_html, re.S)
skin_options = re.findall(r'<option value="([a-z0-9-]+)"', skin_select.group(1)) if skin_select else []
require(skin_options == ["default", *skin_catalog], "settings interface-style selector lists exactly the Rust catalog in order")
prefs_js = read("src/web/js/20-preferences-auth.js")
profile_block = prefs_js.split("const interfaceStyleProfiles = {", 1)[1].split("\n};", 1)[0] if "const interfaceStyleProfiles = {" in prefs_js else ""
profile_keys = re.findall(r'^ "?([a-z0-9-]+)"?: \{$', profile_block, re.M)
require(sorted(profile_keys) == sorted(skin for skin in skin_catalog if skin != "rustbox"), "every predefined style has a fixed JS layout profile")
palette_css = read("src/web/css/55-roundcube-skins.css") + read("src/web/css/56-interface-styles.css")
missing_palettes = [
f"{skin}/{theme}"
for skin in skin_catalog if skin != "rustbox"
for theme in ("light", "dark")
if not re.search(rf'html\[data-interface-skin="{re.escape(skin)}"\]\[data-theme="{theme}"\]\s*\{{[^}}]*--accent:', palette_css)
]
require(not missing_palettes, "every predefined style ships light and dark palettes" + (f" (missing: {', '.join(missing_palettes)})" if missing_palettes else ""))
token_css = read("src/web/css/57-skin-tokens.css")
missing_tokens = [skin for skin in skin_catalog if f'html[data-interface-skin="{skin}"] {{' not in token_css]
require(not missing_tokens, "every interface style declares structural tokens" + (f" (missing: {', '.join(missing_tokens)})" if missing_tokens else ""))
root_tokens = set(re.findall(r"--(skin-[a-z-]+):", token_css.split("}", 1)[0]))
used_tokens = set(re.findall(r"var\(--(skin-[a-z-]+)\)", read("src/web/css/58-skin-immersive.css")))
declared_tokens = set(re.findall(r"--(skin-[a-z-]+):", token_css))
require(used_tokens <= root_tokens and declared_tokens <= root_tokens, "immersive rules only use structural tokens that have :root defaults")
css_manifest = [line.strip() for line in read("src/web/css/manifest.txt").splitlines() if line.strip()]
require(
"57-skin-tokens.css" in css_manifest and "58-skin-immersive.css" in css_manifest
and css_manifest.index("56-interface-styles.css") < css_manifest.index("57-skin-tokens.css") < css_manifest.index("58-skin-immersive.css") < css_manifest.index("60-responsive.css"),
"style tokens and immersive rules load after the palettes and before responsive overrides",
)
require(
'pub const SKIN_DEPTHS: &[&str] = &["colors", "standard", "immersive"];' in display_prefs_rs
and 'skinDepth: "skin_depth"' in prefs_js and 'id="pref-skin-depth"' in index_html
and "root.dataset.skinDepth" in prefs_js and 'savePreference("skinDepth"' in bindings_js,
"style depth is persisted, exposed in Appearance and applied to the document",
)
for _, dialect in MIGRATION_DIALECTS:
require(all(column in read(f"migrations/{dialect}/schema.sql") and column in read(f"src/queries/{dialect}/queries.rs") for column in ("skin_depth", "skin_gradients")), f"{dialect} fresh schema and display query include skin_depth and skin_gradients")
try:
conn = sqlite3.connect(":memory:")
conn.executescript("CREATE TABLE u_prod__display_preferences (id INTEGER PRIMARY KEY, interface_skin TEXT NOT NULL DEFAULT 'rustbox'); INSERT INTO u_prod__display_preferences(id, interface_skin) VALUES(1, 'nord');")
conn.executescript(read("migrations/sqlite/V053__skin_depth_and_gradients.sql").replace("{{p}}", "u_prod__"))
require(conn.execute("SELECT interface_skin, skin_depth, skin_gradients FROM u_prod__display_preferences WHERE id=1").fetchone() == ("nord", "standard", 0), "SQLite V053 adds skin_depth and skin_gradients with standard/off defaults for existing tenants")
conn.close()
except Exception as exc:
errors.append(f"SQLite V053 migration execution failed: {exc}")
require(
"pub skin_gradients: bool" in display_prefs_rs and 'skinGradients: "skin_gradients"' in prefs_js
and 'id="pref-skin-gradients"' in index_html and "root.dataset.skinGradients" in prefs_js
and 'savePreference("skinGradients"' in bindings_js,
"style gradients have their own persisted Appearance switch",
)
# Every gradient in the stylesheets must sit in a rule scoped to the switch.
ungated_gradients = []
for css_name in css_manifest:
css_text = re.sub(r"/\*.*?\*/", "", read(f"src/web/css/{css_name}"), flags=re.S)
for match in re.finditer(r"gradient\(", css_text):
block_start = css_text.rfind("{", 0, match.start())
selector_start = max(css_text.rfind("}", 0, block_start), css_text.rfind(";", 0, block_start))
selector = css_text[selector_start + 1:block_start]
if 'data-skin-gradients="true"' not in selector:
ungated_gradients.append(f"{css_name}: {selector.strip()[:60]}")
require(not ungated_gradients, "every stylesheet gradient is gated by the style-gradients switch" + (f" (ungated: {'; '.join(ungated_gradients[:5])})" if ungated_gradients else ""))
mysql_text_literal_defaults = re.findall(r"^\s*(\w+)\s+TEXT(?:\s+NOT NULL)?\s+DEFAULT\s+'", read("migrations/mysql/schema.sql") + "\n" + "\n".join(read(f"migrations/mysql/{name}") for _, name in MIGRATIONS["mysql"]), re.M)
require(not mysql_text_literal_defaults, "MySQL TEXT columns use expression defaults" + (f" (literal: {', '.join(mysql_text_literal_defaults)})" if mysql_text_literal_defaults else ""))
# TODO/release honesty: live integrations remain explicitly pending.
todo = read("TODO.md")
require("[ ] SILO Compose integration validation" in todo, "TODO keeps live SILO validation pending")
+7 -27
View File
@@ -1345,33 +1345,13 @@ impl AppConfig {
if !matches!(self.default_theme.as_str(), "system" | "dark" | "light") {
return Err("DEFAULT_THEME must be one of: system, dark, light".to_string());
}
if !matches!(
self.default_interface_skin.as_str(),
"rustbox"
| "roundcube-classic"
| "roundcube-larry"
| "roundcube-elastic"
| "clean"
| "gmail"
| "proton"
| "zoho"
| "outlook"
| "minimal"
| "graphite"
| "high-contrast"
| "fastmail"
| "apple-mail"
| "yahoo"
| "hey"
| "nord"
| "github"
| "gitlab"
| "discord"
| "slack"
| "reddit"
| "notion"
) {
return Err("DEFAULT_INTERFACE_SKIN must be one of: rustbox, roundcube-classic, roundcube-larry, roundcube-elastic, clean, gmail, proton, zoho, outlook, minimal, graphite, high-contrast, fastmail, apple-mail, yahoo, hey, nord, github, gitlab, discord, slack, reddit, notion".to_string());
if !crate::db::display_preferences::INTERFACE_SKINS
.contains(&self.default_interface_skin.as_str())
{
return Err(format!(
"DEFAULT_INTERFACE_SKIN must be one of: {}",
crate::db::display_preferences::INTERFACE_SKINS.join(", ")
));
}
if self.sieve_enabled {
if !matches!(
+106 -58
View File
@@ -1,6 +1,53 @@
use crate::db::pool::{Connection, DbValue, ToDbValue};
use serde::{Deserialize, Deserializer, Serialize};
/// Every predefined interface style. `default` (follow the server-wide
/// `default_interface_skin`) is accepted as a user preference in addition to
/// these, but is not itself a style. Shared with configuration validation so
/// the server default and the per-user preference can never drift apart.
pub const INTERFACE_SKINS: &[&str] = &[
"rustbox",
"roundcube-classic",
"roundcube-larry",
"roundcube-elastic",
"clean",
"gmail",
"proton",
"zoho",
"outlook",
"minimal",
"graphite",
"high-contrast",
"fastmail",
"apple-mail",
"yahoo",
"hey",
"nord",
"github",
"gitlab",
"discord",
"slack",
"reddit",
"notion",
"dracula",
"catppuccin",
"solarized",
"gruvbox",
"tokyo-night",
"rose-pine",
"everforest",
"synthwave",
"paper",
"terminal",
"aqua",
"linear",
"material",
"retro",
];
/// How far a predefined interface style may reshape the interface.
pub const SKIN_DEPTHS: &[&str] = &["colors", "standard", "immersive"];
#[derive(Debug, Clone, Serialize)]
pub struct DisplayPreferences {
pub density: String,
@@ -56,6 +103,8 @@ pub struct DisplayPreferences {
pub interface_skin: String,
pub preset_scale_unlocked: bool,
pub adaptive_email_colors: bool,
pub skin_depth: String,
pub skin_gradients: bool,
pub updated_at: String,
}
@@ -115,6 +164,8 @@ pub struct UpdateDisplayPreferences {
pub interface_skin: Option<String>,
pub preset_scale_unlocked: Option<bool>,
pub adaptive_email_colors: Option<bool>,
pub skin_depth: Option<String>,
pub skin_gradients: Option<bool>,
}
fn deserialize_animation_mode_field<'de, D>(
@@ -182,6 +233,8 @@ fn defaults() -> DisplayPreferences {
interface_skin: "default".into(),
preset_scale_unlocked: false,
adaptive_email_colors: false,
skin_depth: "standard".into(),
skin_gradients: false,
updated_at: String::new(),
}
}
@@ -242,7 +295,9 @@ pub fn get_preferences(conn: &Connection) -> Result<DisplayPreferences, String>
interface_skin: row.get(50)?,
preset_scale_unlocked: row.get::<_, i32>(51)? != 0,
adaptive_email_colors: row.get::<_, i32>(52)? != 0,
updated_at: row.get(53)?,
skin_depth: row.get(53)?,
skin_gradients: row.get::<_, i32>(54)? != 0,
updated_at: row.get(55)?,
})
});
match result {
@@ -380,37 +435,13 @@ pub fn update_preferences(
if let Some(ref v) = data.forward_mode {
enum_value("forward_mode", v, &["inline", "attachment"])?;
}
if let Some(ref v) = data.interface_skin {
enum_value(
"interface_skin",
v,
&[
"default",
"rustbox",
"roundcube-classic",
"roundcube-larry",
"roundcube-elastic",
"clean",
"gmail",
"proton",
"zoho",
"outlook",
"minimal",
"graphite",
"high-contrast",
"fastmail",
"apple-mail",
"yahoo",
"hey",
"nord",
"github",
"gitlab",
"discord",
"slack",
"reddit",
"notion",
],
)?;
if let Some(ref v) = data.interface_skin
&& v != "default"
{
enum_value("interface_skin", v, INTERFACE_SKINS)?;
}
if let Some(ref v) = data.skin_depth {
enum_value("skin_depth", v, SKIN_DEPTHS)?;
}
if let Some(v) = data.font_size {
range("font_size", v, 11, 18)?;
@@ -544,6 +575,8 @@ pub fn update_preferences(
set_text!(interface_skin, "interface_skin");
set_bool!(preset_scale_unlocked, "preset_scale_unlocked");
set_bool!(adaptive_email_colors, "adaptive_email_colors");
set_text!(skin_depth, "skin_depth");
set_bool!(skin_gradients, "skin_gradients");
if sets.is_empty() {
return get_preferences(conn);
@@ -610,32 +643,7 @@ mod tests {
#[test]
fn built_in_interface_skins_are_accepted() {
for skin in [
"default",
"rustbox",
"roundcube-classic",
"roundcube-larry",
"roundcube-elastic",
"clean",
"gmail",
"proton",
"zoho",
"outlook",
"minimal",
"graphite",
"high-contrast",
"fastmail",
"apple-mail",
"yahoo",
"hey",
"nord",
"github",
"gitlab",
"discord",
"slack",
"reddit",
"notion",
] {
for skin in std::iter::once("default").chain(INTERFACE_SKINS.iter().copied()) {
let conn = open_test_db();
let update: UpdateDisplayPreferences =
serde_json::from_value(serde_json::json!({"interface_skin": skin})).unwrap();
@@ -660,4 +668,44 @@ mod tests {
serde_json::from_value(serde_json::json!({"accent_color":"red"})).unwrap();
assert!(update_preferences(&conn, &update).is_err());
}
#[test]
fn skin_depth_defaults_to_standard_and_roundtrips() {
let conn = open_test_db();
assert_eq!(get_preferences(&conn).unwrap().skin_depth, "standard");
for depth in SKIN_DEPTHS {
let update: UpdateDisplayPreferences =
serde_json::from_value(serde_json::json!({"skin_depth": depth})).unwrap();
assert_eq!(
update_preferences(&conn, &update).unwrap().skin_depth,
*depth
);
}
}
#[test]
fn skin_gradients_default_off_and_roundtrip() {
let conn = open_test_db();
assert!(!get_preferences(&conn).unwrap().skin_gradients);
let update: UpdateDisplayPreferences =
serde_json::from_value(serde_json::json!({"skin_gradients": true})).unwrap();
assert!(update_preferences(&conn, &update).unwrap().skin_gradients);
}
#[test]
fn invalid_skin_depth_is_rejected() {
let conn = open_test_db();
let update: UpdateDisplayPreferences =
serde_json::from_value(serde_json::json!({"skin_depth":"total"})).unwrap();
assert!(update_preferences(&conn, &update).is_err());
}
#[test]
fn interface_skin_catalog_has_no_duplicates_or_default() {
let mut seen = std::collections::HashSet::new();
for skin in INTERFACE_SKINS {
assert_ne!(*skin, "default");
assert!(seen.insert(*skin), "duplicate interface skin {skin}");
}
}
}
+39 -52
View File
@@ -16,38 +16,23 @@ use tokio::sync::Mutex as AsyncMutex;
use crate::config::AppConfig;
pub const BASE_SCHEMA_VERSION: u32 = 50;
pub const BASE_SCHEMA_VERSION: u32 = 52;
const SQLITE_SCHEMA: &str = include_str!("../../migrations/sqlite/schema.sql");
const MYSQL_SCHEMA: &str = include_str!("../../migrations/mysql/schema.sql");
const PGSQL_SCHEMA: &str = include_str!("../../migrations/pgsql/schema.sql");
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[
(
51,
include_str!(
"../../migrations/sqlite/V051__adaptive_email_colors_and_message_loading_mode.sql"
),
),
(52, include_str!("../../migrations/sqlite/V052__snooze.sql")),
];
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[
(
51,
include_str!(
"../../migrations/mysql/V051__adaptive_email_colors_and_message_loading_mode.sql"
),
),
(52, include_str!("../../migrations/mysql/V052__snooze.sql")),
];
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[
(
51,
include_str!(
"../../migrations/pgsql/V051__adaptive_email_colors_and_message_loading_mode.sql"
),
),
(52, include_str!("../../migrations/pgsql/V052__snooze.sql")),
];
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[(
53,
include_str!("../../migrations/sqlite/V053__skin_depth_and_gradients.sql"),
)];
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[(
53,
include_str!("../../migrations/mysql/V053__skin_depth_and_gradients.sql"),
)];
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[(
53,
include_str!("../../migrations/pgsql/V053__skin_depth_and_gradients.sql"),
)];
const TENANT_TABLES: &[&str] = &[
"blob_delete_outbox",
@@ -1836,7 +1821,7 @@ fn read_legacy_sqlite(path: &Path) -> Result<Vec<LegacyTableData>, String> {
.map_err(|e| format!("failed to read legacy SQLite schema version: {e}"))?;
if version < BASE_SCHEMA_VERSION {
return Err(format!(
"legacy SQLite schema version {version} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with a V050-capable RustBox release first"
"legacy SQLite schema version {version} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with a V{BASE_SCHEMA_VERSION:03}-capable RustBox release first"
));
}
let latest_supported_sqlite = SQLITE_MIGRATIONS
@@ -2197,7 +2182,7 @@ fn apply_sqlite_migrations(
) -> Result<(), String> {
if current < BASE_SCHEMA_VERSION {
return Err(format!(
"database schema version {current} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with a V050-capable RustBox release first"
"database schema version {current} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with a V{BASE_SCHEMA_VERSION:03}-capable RustBox release first"
));
}
let latest = latest_schema_version();
@@ -2642,19 +2627,17 @@ mod tests {
}
#[test]
fn sqlite_v051_migrates_existing_v050_preferences() {
fn sqlite_v053_adds_skin_depth_and_gradients_to_existing_baseline_tenants() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE rustbox_accounts (user_hash TEXT PRIMARY KEY, table_prefix TEXT NOT NULL, schema_version INTEGER NOT NULL, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);\n\
CREATE TABLE u_test__display_preferences (id INTEGER PRIMARY KEY, theme TEXT NOT NULL DEFAULT 'system');\n\
CREATE TABLE u_test__mail_preferences (id INTEGER PRIMARY KEY, messages_per_page INTEGER NOT NULL DEFAULT 300);\n\
INSERT INTO rustbox_accounts(user_hash, table_prefix, schema_version) VALUES('test', 'u_test__', 50);\n\
INSERT INTO u_test__display_preferences(id) VALUES(1);\n\
INSERT INTO u_test__mail_preferences(id) VALUES(1);",
CREATE TABLE u_test__display_preferences (id INTEGER PRIMARY KEY, theme TEXT NOT NULL DEFAULT 'system', interface_skin TEXT NOT NULL DEFAULT 'rustbox');\n\
INSERT INTO rustbox_accounts(user_hash, table_prefix, schema_version) VALUES('test', 'u_test__', 52);\n\
INSERT INTO u_test__display_preferences(id, interface_skin) VALUES(1, 'nord');",
)
.unwrap();
apply_sqlite_migrations(&conn, "u_test__", 50).unwrap();
apply_sqlite_migrations(&conn, "u_test__", BASE_SCHEMA_VERSION).unwrap();
let version: u32 = conn
.query_row(
@@ -2663,28 +2646,30 @@ mod tests {
|row| row.get(0),
)
.unwrap();
assert_eq!(version, 52);
assert_eq!(version, latest_schema_version());
let adaptive: i64 = conn
let (skin, depth, gradients): (String, String, i64) = conn
.query_row(
"SELECT adaptive_email_colors FROM u_test__display_preferences WHERE id=1",
"SELECT interface_skin, skin_depth, skin_gradients FROM u_test__display_preferences WHERE id=1",
[],
|row| row.get(0),
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.unwrap();
let mode: String = conn
.query_row(
"SELECT message_loading_mode FROM u_test__mail_preferences WHERE id=1",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(adaptive, 0);
assert_eq!(mode, "pagination");
assert_eq!(skin, "nord");
assert_eq!(depth, "standard");
assert_eq!(gradients, 0);
}
#[test]
fn legacy_sqlite_v050_is_imported_and_backed_up() {
fn sqlite_migrations_reject_tenants_older_than_the_baseline() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
let error =
apply_sqlite_migrations(&conn, "u_test__", BASE_SCHEMA_VERSION - 1).unwrap_err();
assert!(error.contains("older than the consolidated RustBox baseline"));
}
#[test]
fn legacy_sqlite_baseline_is_imported_and_backed_up() {
let temp = tempfile::tempdir().unwrap();
let data_dir = temp.path();
let user_hash = format!("{:x}", Sha256::digest(b"legacy-user"));
@@ -2700,7 +2685,9 @@ mod tests {
legacy
.execute_batch(&tenant_seed_sql(DatabaseBackend::Sqlite, ""))
.unwrap();
legacy.execute_batch("PRAGMA user_version=50;").unwrap();
legacy
.execute_batch(&format!("PRAGMA user_version={BASE_SCHEMA_VERSION};"))
.unwrap();
legacy.execute(
"INSERT INTO display_preferences(id, theme, accent_color) VALUES(1, 'dark', '#123456')",
[],
+1 -1
View File
@@ -127,7 +127,7 @@ pub mod db_contacts {
}
pub mod db_display_preferences {
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, adaptive_email_colors, updated_at FROM display_preferences WHERE id = 1";
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, adaptive_email_colors, skin_depth, skin_gradients, updated_at FROM display_preferences WHERE id = 1";
pub const Q_002: &str = "INSERT IGNORE INTO display_preferences (id) VALUES (1)";
pub const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ?";
}
+1 -1
View File
@@ -127,7 +127,7 @@ pub mod db_contacts {
}
pub mod db_display_preferences {
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, adaptive_email_colors, updated_at FROM display_preferences WHERE id = 1";
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, adaptive_email_colors, skin_depth, skin_gradients, updated_at FROM display_preferences WHERE id = 1";
pub const Q_002: &str =
"INSERT INTO display_preferences (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
pub const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ${}";
+1 -1
View File
@@ -125,7 +125,7 @@ pub mod db_contacts {
}
pub mod db_display_preferences {
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, adaptive_email_colors, updated_at FROM display_preferences WHERE id = 1";
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, adaptive_email_colors, skin_depth, skin_gradients, updated_at FROM display_preferences WHERE id = 1";
pub const Q_002: &str = "INSERT OR IGNORE INTO display_preferences (id) VALUES (1)";
pub const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ?{}";
}
+39 -8
View File
@@ -76,7 +76,7 @@ html[data-interface-skin="roundcube-classic"][data-theme="light"] .pane-toolbar,
html[data-interface-skin="roundcube-classic"][data-theme="light"] .reader-toolbar,
html[data-interface-skin="roundcube-classic"][data-theme="light"] .dialog-header,
html[data-interface-skin="roundcube-classic"][data-theme="light"] .section-heading {
background: linear-gradient(#f4f4f4, #cccccc);
background: #dddddd;
border-color: #999999;
color: #333333;
}
@@ -86,7 +86,7 @@ html[data-interface-skin="roundcube-classic"][data-theme="dark"] .pane-toolbar,
html[data-interface-skin="roundcube-classic"][data-theme="dark"] .reader-toolbar,
html[data-interface-skin="roundcube-classic"][data-theme="dark"] .dialog-header,
html[data-interface-skin="roundcube-classic"][data-theme="dark"] .section-heading {
background: linear-gradient(#3a3a3a, #292929);
background: #313131;
border-color: #666666;
color: #f2f2f2;
}
@@ -129,20 +129,20 @@ html[data-interface-skin="roundcube-classic"] .appearance-preset {
html[data-interface-skin="roundcube-classic"][data-theme="light"] .button,
html[data-interface-skin="roundcube-classic"][data-theme="light"] .button.primary {
border-color: #a4a4a4;
background: linear-gradient(#ffffff, #e2e2e2);
background: #f0f0f0;
color: #333333;
}
html[data-interface-skin="roundcube-classic"][data-theme="light"] .button:hover,
html[data-interface-skin="roundcube-classic"][data-theme="light"] .button.primary:hover {
border-color: #777777;
background: linear-gradient(#ffffff, #d5d5d5);
background: #e4e4e4;
}
html[data-interface-skin="roundcube-classic"][data-theme="dark"] .button,
html[data-interface-skin="roundcube-classic"][data-theme="dark"] .button.primary {
border-color: #6f6f6f;
background: linear-gradient(#3c3c3c, #292929);
background: #333333;
color: #f2f2f2;
}
@@ -409,12 +409,12 @@ html[data-interface-skin="roundcube-elastic"] .appearance-preset:hover {
html[data-interface-skin="roundcube-classic"] .folder-drawer-header {
min-height: 32px;
background: linear-gradient(#f4f4f4, #cccccc);
background: #dddddd;
border-color: #999999;
}
html[data-interface-skin="roundcube-classic"][data-theme="dark"] .folder-drawer-header {
background: linear-gradient(#3a3a3a, #292929);
background: #313131;
border-color: #666666;
}
@@ -426,4 +426,35 @@ html[data-interface-skin="roundcube-elastic"] .appearance-preset:hover {
html[data-interface-skin="roundcube-elastic"][data-theme="dark"] .folder-drawer-header {
background: #2f3a3f;
}
}
}
/* Roundcube Classic bevel gradients: only with the Appearance "gradients" switch on. */
html[data-skin-gradients="true"][data-interface-skin="roundcube-classic"][data-theme="light"] :is(.appbar, .pane-toolbar, .reader-toolbar, .dialog-header, .section-heading) {
background: linear-gradient(#f4f4f4, #cccccc);
}
html[data-skin-gradients="true"][data-interface-skin="roundcube-classic"][data-theme="dark"] :is(.appbar, .pane-toolbar, .reader-toolbar, .dialog-header, .section-heading) {
background: linear-gradient(#3a3a3a, #292929);
}
html[data-skin-gradients="true"][data-interface-skin="roundcube-classic"][data-theme="light"] :is(.button, .button.primary) {
background: linear-gradient(#ffffff, #e2e2e2);
}
html[data-skin-gradients="true"][data-interface-skin="roundcube-classic"][data-theme="light"] :is(.button, .button.primary):hover {
background: linear-gradient(#ffffff, #d5d5d5);
}
html[data-skin-gradients="true"][data-interface-skin="roundcube-classic"][data-theme="dark"] :is(.button, .button.primary) {
background: linear-gradient(#3c3c3c, #292929);
}
@media (max-width: 820px) {
html[data-skin-gradients="true"][data-interface-skin="roundcube-classic"][data-theme="light"] .folder-drawer-header {
background: linear-gradient(#f4f4f4, #cccccc);
}
html[data-skin-gradients="true"][data-interface-skin="roundcube-classic"][data-theme="dark"] .folder-drawer-header {
background: linear-gradient(#3a3a3a, #292929);
}
}
+759
View File
@@ -1094,3 +1094,762 @@ html[data-interface-skin="notion"][data-theme="dark"] {
--field: #191919; --selected: #353535; --hover: #2b2b2b; --shadow: rgba(0, 0, 0, .38);
--folder-bg: #202020; --message-bg: #202020; --reader-bg: #202020;
}
/* 0.2.23 predefined styles: light and dark palettes. */
html[data-interface-skin="dracula"][data-theme="light"] {
color-scheme: light;
--bg: #f4f1e6;
--panel: #fffbeb;
--panel-2: #f8f4e2;
--panel-3: #ece6cf;
--sidebar: #f1ecd8;
--line: #ddd5bb;
--line-strong: #b8ae8d;
--text: #1f1f1f;
--muted: #5f5a6e;
--subtle: #7a7490;
--accent: #644ac9;
--accent-strong: #4f36b0;
--accent-ink: #ffffff;
--danger: #cb3a2a;
--warning: #846e15;
--success: #14710a;
--field: #fffdf4;
--selected: #e6dcfb;
--hover: #f3edd8;
--shadow: rgba(40, 30, 70, .14);
--folder-bg: #f1ecd8;
--message-bg: #fffbeb;
--reader-bg: #fffbeb;
}
html[data-interface-skin="dracula"][data-theme="dark"] {
color-scheme: dark;
--bg: #21222c;
--panel: #282a36;
--panel-2: #2f3241;
--panel-3: #383a4c;
--sidebar: #21222c;
--line: #3a3d4f;
--line-strong: #6272a4;
--text: #f8f8f2;
--muted: #c3c6d9;
--subtle: #8a8fb5;
--accent: #bd93f9;
--accent-strong: #d0b0ff;
--accent-ink: #1d1530;
--danger: #ff5555;
--warning: #ffb86c;
--success: #50fa7b;
--field: #21222c;
--selected: #44475a;
--hover: #303344;
--shadow: rgba(0, 0, 0, .5);
--folder-bg: #21222c;
--message-bg: #282a36;
--reader-bg: #282a36;
}
html[data-interface-skin="catppuccin"][data-theme="light"] {
color-scheme: light;
--bg: #dce0e8;
--panel: #eff1f5;
--panel-2: #e6e9ef;
--panel-3: #ccd0da;
--sidebar: #e6e9ef;
--line: #ccd0da;
--line-strong: #acb0be;
--text: #4c4f69;
--muted: #5c5f77;
--subtle: #7c7f93;
--accent: #8839ef;
--accent-strong: #6e1fd6;
--accent-ink: #ffffff;
--danger: #d20f39;
--warning: #a86c0f;
--success: #40a02b;
--field: #eff1f5;
--selected: #e2d4fb;
--hover: #e6e9ef;
--shadow: rgba(76, 79, 105, .14);
--folder-bg: #e6e9ef;
--message-bg: #eff1f5;
--reader-bg: #eff1f5;
}
html[data-interface-skin="catppuccin"][data-theme="dark"] {
color-scheme: dark;
--bg: #11111b;
--panel: #1e1e2e;
--panel-2: #181825;
--panel-3: #313244;
--sidebar: #181825;
--line: #313244;
--line-strong: #45475a;
--text: #cdd6f4;
--muted: #a6adc8;
--subtle: #7f849c;
--accent: #cba6f7;
--accent-strong: #dcc4fb;
--accent-ink: #1e1e2e;
--danger: #f38ba8;
--warning: #f9e2af;
--success: #a6e3a1;
--field: #181825;
--selected: #3a3350;
--hover: #27273a;
--shadow: rgba(0, 0, 0, .45);
--folder-bg: #181825;
--message-bg: #1e1e2e;
--reader-bg: #1e1e2e;
}
html[data-interface-skin="solarized"][data-theme="light"] {
color-scheme: light;
--bg: #eee8d5;
--panel: #fdf6e3;
--panel-2: #f7f0dc;
--panel-3: #eee8d5;
--sidebar: #f5eedb;
--line: #e0d8bf;
--line-strong: #c4bb9f;
--text: #073642;
--muted: #586e75;
--subtle: #7b8b8f;
--accent: #268bd2;
--accent-strong: #1a6fae;
--accent-ink: #fdf6e3;
--danger: #dc322f;
--warning: #9a7500;
--success: #6b7c00;
--field: #fffbf0;
--selected: #dbe7ec;
--hover: #f2ebd6;
--shadow: rgba(7, 54, 66, .14);
--folder-bg: #f5eedb;
--message-bg: #fdf6e3;
--reader-bg: #fdf6e3;
}
html[data-interface-skin="solarized"][data-theme="dark"] {
color-scheme: dark;
--bg: #00212b;
--panel: #002b36;
--panel-2: #073642;
--panel-3: #0b404e;
--sidebar: #00252f;
--line: #0f4553;
--line-strong: #2f5f6b;
--text: #eee8d5;
--muted: #93a1a1;
--subtle: #6f8a90;
--accent: #268bd2;
--accent-strong: #4ba3e3;
--accent-ink: #fdf6e3;
--danger: #dc322f;
--warning: #b58900;
--success: #859900;
--field: #00252f;
--selected: #0d4a5a;
--hover: #06323d;
--shadow: rgba(0, 0, 0, .45);
--folder-bg: #00252f;
--message-bg: #002b36;
--reader-bg: #002b36;
}
html[data-interface-skin="gruvbox"][data-theme="light"] {
color-scheme: light;
--bg: #f2e5bc;
--panel: #fbf1c7;
--panel-2: #f9f5d7;
--panel-3: #ebdbb2;
--sidebar: #f2e5bc;
--line: #d5c4a1;
--line-strong: #bdae93;
--text: #3c3836;
--muted: #665c54;
--subtle: #7c6f64;
--accent: #af3a03;
--accent-strong: #8f2f02;
--accent-ink: #fbf1c7;
--danger: #9d0006;
--warning: #b57614;
--success: #79740e;
--field: #f9f5d7;
--selected: #ebdbb2;
--hover: #f2e5bc;
--shadow: rgba(60, 56, 54, .16);
--folder-bg: #f2e5bc;
--message-bg: #fbf1c7;
--reader-bg: #fbf1c7;
}
html[data-interface-skin="gruvbox"][data-theme="dark"] {
color-scheme: dark;
--bg: #1d2021;
--panel: #282828;
--panel-2: #32302f;
--panel-3: #3c3836;
--sidebar: #1d2021;
--line: #3c3836;
--line-strong: #665c54;
--text: #ebdbb2;
--muted: #bdae93;
--subtle: #928374;
--accent: #fe8019;
--accent-strong: #fe9a4a;
--accent-ink: #1d2021;
--danger: #fb4934;
--warning: #fabd2f;
--success: #b8bb26;
--field: #1d2021;
--selected: #504945;
--hover: #32302f;
--shadow: rgba(0, 0, 0, .5);
--folder-bg: #1d2021;
--message-bg: #282828;
--reader-bg: #282828;
}
html[data-interface-skin="tokyo-night"][data-theme="light"] {
color-scheme: light;
--bg: #d0d5e3;
--panel: #e1e2e7;
--panel-2: #e9e9ed;
--panel-3: #c4c8da;
--sidebar: #dadbe2;
--line: #c4c8da;
--line-strong: #a8aecb;
--text: #3760bf;
--muted: #4a5a9a;
--subtle: #6c75a0;
--accent: #2e7de9;
--accent-strong: #1f62c2;
--accent-ink: #ffffff;
--danger: #c41b4f;
--warning: #8c6c3e;
--success: #587539;
--field: #e9e9ed;
--selected: #dde4f8;
--hover: #d5d8e4;
--shadow: rgba(55, 96, 191, .14);
--folder-bg: #dadbe2;
--message-bg: #e1e2e7;
--reader-bg: #e9e9ed;
}
html[data-interface-skin="tokyo-night"][data-theme="dark"] {
color-scheme: dark;
--bg: #16161e;
--panel: #1a1b26;
--panel-2: #1f2335;
--panel-3: #292e42;
--sidebar: #16161e;
--line: #292e42;
--line-strong: #414868;
--text: #c0caf5;
--muted: #a9b1d6;
--subtle: #737aa2;
--accent: #7aa2f7;
--accent-strong: #9ab8ff;
--accent-ink: #16161e;
--danger: #f7768e;
--warning: #e0af68;
--success: #9ece6a;
--field: #16161e;
--selected: #2e3c64;
--hover: #222436;
--shadow: rgba(0, 0, 0, .5);
--folder-bg: #16161e;
--message-bg: #1a1b26;
--reader-bg: #1a1b26;
}
html[data-interface-skin="rose-pine"][data-theme="light"] {
color-scheme: light;
--bg: #f2e9e1;
--panel: #fffaf3;
--panel-2: #faf4ed;
--panel-3: #f2e9e1;
--sidebar: #faf4ed;
--line: #dfdad9;
--line-strong: #cecacd;
--text: #575279;
--muted: #6e6a8a;
--subtle: #8a859b;
--accent: #b4637a;
--accent-strong: #9a4f66;
--accent-ink: #fffaf3;
--danger: #c74b4b;
--warning: #b9781f;
--success: #286983;
--field: #fffaf3;
--selected: #f4dcd6;
--hover: #f4ede8;
--shadow: rgba(87, 82, 121, .12);
--folder-bg: #faf4ed;
--message-bg: #fffaf3;
--reader-bg: #fffaf3;
}
html[data-interface-skin="rose-pine"][data-theme="dark"] {
color-scheme: dark;
--bg: #191724;
--panel: #1f1d2e;
--panel-2: #21202e;
--panel-3: #26233a;
--sidebar: #191724;
--line: #26233a;
--line-strong: #403d52;
--text: #e0def4;
--muted: #908caa;
--subtle: #6e6a86;
--accent: #ebbcba;
--accent-strong: #f3d3d1;
--accent-ink: #191724;
--danger: #eb6f92;
--warning: #f6c177;
--success: #9ccfd8;
--field: #191724;
--selected: #403d52;
--hover: #26233a;
--shadow: rgba(0, 0, 0, .45);
--folder-bg: #191724;
--message-bg: #1f1d2e;
--reader-bg: #1f1d2e;
}
html[data-interface-skin="everforest"][data-theme="light"] {
color-scheme: light;
--bg: #efebd4;
--panel: #fdf6e3;
--panel-2: #f4f0d9;
--panel-3: #e6e2cc;
--sidebar: #f4f0d9;
--line: #e0dcc7;
--line-strong: #bdc3af;
--text: #5c6a72;
--muted: #5f6f68;
--subtle: #7f8c80;
--accent: #2f8f6a;
--accent-strong: #24765a;
--accent-ink: #fdf6e3;
--danger: #e8443f;
--warning: #b88300;
--success: #6c7d00;
--field: #fffbef;
--selected: #e3ecd3;
--hover: #f0ecd6;
--shadow: rgba(92, 106, 114, .14);
--folder-bg: #f4f0d9;
--message-bg: #fdf6e3;
--reader-bg: #fdf6e3;
}
html[data-interface-skin="everforest"][data-theme="dark"] {
color-scheme: dark;
--bg: #232a2e;
--panel: #2d353b;
--panel-2: #343f44;
--panel-3: #3d484d;
--sidebar: #232a2e;
--line: #3d484d;
--line-strong: #4f585e;
--text: #d3c6aa;
--muted: #9da9a0;
--subtle: #859289;
--accent: #a7c080;
--accent-strong: #bccf9c;
--accent-ink: #232a2e;
--danger: #e67e80;
--warning: #dbbc7f;
--success: #83c092;
--field: #232a2e;
--selected: #425047;
--hover: #343f44;
--shadow: rgba(0, 0, 0, .4);
--folder-bg: #232a2e;
--message-bg: #2d353b;
--reader-bg: #2d353b;
}
html[data-interface-skin="synthwave"][data-theme="light"] {
color-scheme: light;
--bg: #f3e9ff;
--panel: #fffaff;
--panel-2: #f9f1ff;
--panel-3: #efe1fb;
--sidebar: #f5eaff;
--line: #e4d3f5;
--line-strong: #c7aee6;
--text: #2b1745;
--muted: #66508a;
--subtle: #8b78a8;
--accent: #d6249f;
--accent-strong: #b0137f;
--accent-ink: #ffffff;
--danger: #d61f3f;
--warning: #9a6b00;
--success: #0f8a64;
--field: #fffaff;
--selected: #fadcf2;
--hover: #f6ebfd;
--shadow: rgba(120, 40, 160, .16);
--folder-bg: #f5eaff;
--message-bg: #fffaff;
--reader-bg: #fffaff;
}
html[data-interface-skin="synthwave"][data-theme="dark"] {
color-scheme: dark;
--bg: #1a1029;
--panel: #241b2f;
--panel-2: #2a2139;
--panel-3: #34294f;
--sidebar: #1b1130;
--line: #3b2d59;
--line-strong: #5a4682;
--text: #f4eefe;
--muted: #c8b8e6;
--subtle: #9a87c2;
--accent: #ff7edb;
--accent-strong: #ff9de4;
--accent-ink: #2a0d23;
--danger: #fe4450;
--warning: #fede5d;
--success: #72f1b8;
--field: #1a1029;
--selected: #44306b;
--hover: #2f2445;
--shadow: rgba(0, 0, 0, .5);
--folder-bg: #1b1130;
--message-bg: #241b2f;
--reader-bg: #241b2f;
}
html[data-interface-skin="paper"][data-theme="light"] {
color-scheme: light;
--bg: #ece6d9;
--panel: #faf7f0;
--panel-2: #f4efe4;
--panel-3: #e9e2d2;
--sidebar: #f1ebdf;
--line: #ddd4c2;
--line-strong: #b9ad95;
--text: #2b2620;
--muted: #5e554a;
--subtle: #847a6c;
--accent: #8a3b2c;
--accent-strong: #6f2d21;
--accent-ink: #faf7f0;
--danger: #a1301f;
--warning: #8a6418;
--success: #4d6b2e;
--field: #fffdf8;
--selected: #eadfca;
--hover: #f1eadc;
--shadow: rgba(60, 45, 25, .14);
--folder-bg: #f1ebdf;
--message-bg: #faf7f0;
--reader-bg: #fffdf8;
}
html[data-interface-skin="paper"][data-theme="dark"] {
color-scheme: dark;
--bg: #1c1915;
--panel: #24201b;
--panel-2: #2a251f;
--panel-3: #332d25;
--sidebar: #1f1b17;
--line: #3a3229;
--line-strong: #5a4f40;
--text: #e8dfcf;
--muted: #b8ab95;
--subtle: #8d816f;
--accent: #d79b6b;
--accent-strong: #e6b48a;
--accent-ink: #22170e;
--danger: #e2826f;
--warning: #d8b26a;
--success: #a5b980;
--field: #1f1b17;
--selected: #42372a;
--hover: #2c2620;
--shadow: rgba(0, 0, 0, .45);
--folder-bg: #1f1b17;
--message-bg: #24201b;
--reader-bg: #24201b;
}
html[data-interface-skin="terminal"][data-theme="light"] {
color-scheme: light;
--bg: #e4ecdf;
--panel: #f4f8f1;
--panel-2: #eaf2e5;
--panel-3: #dce8d5;
--sidebar: #ecf3e8;
--line: #cbdcc3;
--line-strong: #97b48a;
--text: #10240f;
--muted: #3c5a36;
--subtle: #5d7a56;
--accent: #1f7a2e;
--accent-strong: #165f22;
--accent-ink: #f4f8f1;
--danger: #b3261e;
--warning: #7a6000;
--success: #1f7a2e;
--field: #fbfdf9;
--selected: #cfe8c9;
--hover: #e3eedd;
--shadow: rgba(16, 36, 15, .14);
--folder-bg: #ecf3e8;
--message-bg: #f4f8f1;
--reader-bg: #fbfdf9;
}
html[data-interface-skin="terminal"][data-theme="dark"] {
color-scheme: dark;
--bg: #050805;
--panel: #0a0f0a;
--panel-2: #0e150e;
--panel-3: #142014;
--sidebar: #070b07;
--line: #173117;
--line-strong: #2a5a2a;
--text: #a8f0a8;
--muted: #6fc46f;
--subtle: #4f8f4f;
--accent: #39ff6a;
--accent-strong: #7dff9b;
--accent-ink: #021003;
--danger: #ff5f56;
--warning: #ffd866;
--success: #39ff6a;
--field: #050805;
--selected: #123d1a;
--hover: #0f1d0f;
--shadow: rgba(0, 0, 0, .6);
--folder-bg: #070b07;
--message-bg: #0a0f0a;
--reader-bg: #0a0f0a;
}
html[data-interface-skin="aqua"][data-theme="light"] {
color-scheme: light;
--bg: #e6eef8;
--panel: #ffffff;
--panel-2: #f5f8fc;
--panel-3: #e8eef6;
--sidebar: #eef3f9;
--line: #d6e0ec;
--line-strong: #b2c2d6;
--text: #10213a;
--muted: #4f6582;
--subtle: #7488a3;
--accent: #0a84ff;
--accent-strong: #0069d9;
--accent-ink: #ffffff;
--danger: #e0383e;
--warning: #9a6a00;
--success: #23a047;
--field: #ffffff;
--selected: #d4e7ff;
--hover: #edf3fa;
--shadow: rgba(16, 33, 58, .14);
--folder-bg: #eef3f9;
--message-bg: #ffffff;
--reader-bg: #ffffff;
}
html[data-interface-skin="aqua"][data-theme="dark"] {
color-scheme: dark;
--bg: #0b1220;
--panel: #141c2b;
--panel-2: #182132;
--panel-3: #1f2a3e;
--sidebar: #101827;
--line: #24314a;
--line-strong: #3a4a67;
--text: #eaf1fb;
--muted: #a7b6cc;
--subtle: #7d8ea8;
--accent: #4aa3ff;
--accent-strong: #74b9ff;
--accent-ink: #04121f;
--danger: #ff6961;
--warning: #ffd60a;
--success: #32d74b;
--field: #0f1726;
--selected: #1e3a5f;
--hover: #1a2436;
--shadow: rgba(0, 0, 0, .45);
--folder-bg: #101827;
--message-bg: #141c2b;
--reader-bg: #141c2b;
}
html[data-interface-skin="linear"][data-theme="light"] {
color-scheme: light;
--bg: #f4f5f8;
--panel: #ffffff;
--panel-2: #f9fafb;
--panel-3: #eef0f3;
--sidebar: #f4f5f8;
--line: #e4e6ea;
--line-strong: #c9ccd3;
--text: #1b1c1f;
--muted: #5f636b;
--subtle: #8a8f98;
--accent: #5e6ad2;
--accent-strong: #4a55b8;
--accent-ink: #ffffff;
--danger: #d93f3f;
--warning: #a8780a;
--success: #2f8f5e;
--field: #ffffff;
--selected: #e7e9fb;
--hover: #f1f2f5;
--shadow: rgba(20, 22, 40, .10);
--folder-bg: #f4f5f8;
--message-bg: #ffffff;
--reader-bg: #ffffff;
}
html[data-interface-skin="linear"][data-theme="dark"] {
color-scheme: dark;
--bg: #08090a;
--panel: #0f1011;
--panel-2: #141516;
--panel-3: #1c1d1f;
--sidebar: #0b0c0d;
--line: #202123;
--line-strong: #34363a;
--text: #f7f8f8;
--muted: #a8abb2;
--subtle: #7c7f88;
--accent: #5e6ad2;
--accent-strong: #7c86e0;
--accent-ink: #ffffff;
--danger: #eb5757;
--warning: #f2c94c;
--success: #4cb782;
--field: #0b0c0d;
--selected: #1d1f33;
--hover: #17181a;
--shadow: rgba(0, 0, 0, .6);
--folder-bg: #0b0c0d;
--message-bg: #0f1011;
--reader-bg: #0f1011;
}
html[data-interface-skin="material"][data-theme="light"] {
color-scheme: light;
--bg: #f7f2fa;
--panel: #fffbfe;
--panel-2: #f3edf7;
--panel-3: #ece6f0;
--sidebar: #f3edf7;
--line: #e7e0ec;
--line-strong: #cac4d0;
--text: #1d1b20;
--muted: #49454f;
--subtle: #79747e;
--accent: #6750a4;
--accent-strong: #4f378b;
--accent-ink: #ffffff;
--danger: #b3261e;
--warning: #7d5700;
--success: #386a20;
--field: #fffbfe;
--selected: #e8def8;
--hover: #f2ecf7;
--shadow: rgba(29, 27, 32, .14);
--folder-bg: #f3edf7;
--message-bg: #fffbfe;
--reader-bg: #fffbfe;
}
html[data-interface-skin="material"][data-theme="dark"] {
color-scheme: dark;
--bg: #141218;
--panel: #1d1b20;
--panel-2: #211f26;
--panel-3: #2b2930;
--sidebar: #1d1b20;
--line: #36343b;
--line-strong: #49454f;
--text: #e6e0e9;
--muted: #cac4d0;
--subtle: #938f99;
--accent: #d0bcff;
--accent-strong: #e2d5ff;
--accent-ink: #381e72;
--danger: #f2b8b5;
--warning: #f9d17a;
--success: #9fd67f;
--field: #141218;
--selected: #4a4458;
--hover: #27252b;
--shadow: rgba(0, 0, 0, .45);
--folder-bg: #1d1b20;
--message-bg: #1d1b20;
--reader-bg: #211f26;
}
html[data-interface-skin="retro"][data-theme="light"] {
color-scheme: light;
--bg: #c0c0c0;
--panel: #ffffff;
--panel-2: #dfdfdf;
--panel-3: #c0c0c0;
--sidebar: #c0c0c0;
--line: #808080;
--line-strong: #404040;
--text: #000000;
--muted: #303030;
--subtle: #555555;
--accent: #000080;
--accent-strong: #1084d0;
--accent-ink: #ffffff;
--danger: #a00000;
--warning: #806000;
--success: #006000;
--field: #ffffff;
--selected: #a6caf0;
--hover: #e8e8e8;
--shadow: rgba(0, 0, 0, .35);
--folder-bg: #c0c0c0;
--message-bg: #ffffff;
--reader-bg: #ffffff;
}
html[data-interface-skin="retro"][data-theme="dark"] {
color-scheme: dark;
--bg: #1b1b1f;
--panel: #232327;
--panel-2: #2c2c31;
--panel-3: #36363c;
--sidebar: #2c2c31;
--line: #55555c;
--line-strong: #7c7c84;
--text: #ececec;
--muted: #b8b8bf;
--subtle: #8c8c94;
--accent: #3a6fd8;
--accent-strong: #5c8be6;
--accent-ink: #ffffff;
--danger: #ff6b6b;
--warning: #e0c060;
--success: #6fd36f;
--field: #18181b;
--selected: #2b3f6b;
--hover: #2f2f35;
--shadow: rgba(0, 0, 0, .5);
--folder-bg: #2c2c31;
--message-bg: #232327;
--reader-bg: #232327;
}
+896
View File
@@ -0,0 +1,896 @@
/*
* RustBox interface-style standard (0.2.23).
*
* Every predefined style ships three layers:
* 1. light and dark palettes (56-interface-styles.css / 55-roundcube-skins.css);
* 2. a fixed layout profile (interfaceStyleProfiles in 20-preferences-auth.js);
* 3. the structural tokens below, consumed only when the user selects
* "Immersive" style depth (58-skin-immersive.css).
*
* Gradients are opt-in through their own Appearance switch (skin_gradients):
* every gradient token has a flat value here and its gradient form only in a
* block scoped to html[data-skin-gradients="true"].
*
* Generated from one table; keep every style listed here.
*/
:root {
--skin-font: var(--font-sans);
--skin-font-heading: var(--skin-font);
--skin-heading-weight: 680;
--skin-heading-tracking: -0.01em;
--skin-heading-transform: none;
--skin-section-transform: uppercase;
--skin-section-tracking: .08em;
--skin-section-variant: normal;
--skin-radius-control: 8px;
--skin-radius-button: var(--skin-radius-control);
--skin-radius-compose: var(--skin-radius-button);
--skin-radius-panel: 12px;
--skin-radius-row: 8px;
--skin-folder-radius: var(--skin-radius-row);
--skin-radius-avatar: 50%;
--skin-shell-gap: 8px;
--skin-shell-padding: 0 8px 8px;
--skin-backdrop: var(--bg);
--skin-glass: none;
--skin-pane-border: 1px solid var(--line);
--skin-pane-divider: var(--skin-pane-border);
--skin-pane-shadow: 0 1px 2px var(--shadow);
--skin-pane-bg: var(--message-bg, var(--panel));
--skin-reader-bg: var(--reader-bg, var(--panel));
--skin-sidebar-bg: var(--folder-bg, var(--sidebar));
--skin-sidebar-border: var(--skin-pane-border);
--skin-sidebar-shadow: var(--skin-pane-shadow);
--skin-sidebar-fg: var(--text);
--skin-sidebar-muted: var(--muted);
--skin-sidebar-hover: var(--hover);
--skin-sidebar-accent: var(--accent);
--skin-appbar-bg: var(--bg);
--skin-appbar-fg: var(--text);
--skin-appbar-border: transparent;
--skin-appbar-shadow: none;
--skin-search-bg: var(--field);
--skin-search-fg: var(--text);
--skin-search-border: var(--line);
--skin-search-radius: var(--skin-radius-control);
--skin-field-shadow: none;
--skin-selected-bg: var(--selected);
--skin-selected-fg: var(--text);
--skin-selected-muted: var(--muted);
--skin-marker-width: 0px;
--skin-marker-color: var(--accent);
--skin-row-divider: var(--line);
--skin-row-inset: 0px;
--skin-row-gap: 0px;
--skin-folder-active-bg: var(--skin-selected-bg);
--skin-folder-active-fg: var(--skin-selected-fg);
--skin-primary-bg: var(--accent);
--skin-primary-fg: var(--accent-ink);
--skin-primary-shadow: none;
--skin-compose-bg: var(--skin-primary-bg);
--skin-compose-fg: var(--skin-primary-fg);
--skin-compose-shadow: var(--skin-primary-shadow);
--skin-button-weight: 600;
--skin-control-border: var(--line-strong);
--skin-control-shadow: none;
--skin-focus-ring: 0 0 0 3px color-mix(in srgb, var(--accent) 32%, transparent);
}
html[data-interface-skin="rustbox"] {
/* RustBox custom style: uses the defaults above with your own colors. */
}
html[data-interface-skin="roundcube-classic"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line-strong);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-radius-control: 2px;
--skin-radius-row: 0px;
--skin-radius-avatar: 2px;
--skin-heading-weight: 700;
--skin-heading-tracking: 0;
--skin-appbar-border: var(--line-strong);
--skin-primary-bg: var(--accent);
--skin-control-shadow: inset 0 1px 0 rgba(255, 255, 255, .28);
--skin-focus-ring: 0 0 0 1px var(--accent);
}
html[data-interface-skin="roundcube-classic"][data-theme="light"] {
--skin-appbar-bg: #dddddd;
--skin-appbar-fg: #333333;
}
html[data-interface-skin="roundcube-classic"][data-theme="dark"] {
--skin-appbar-bg: #313131;
--skin-appbar-fg: #f2f2f2;
}
html[data-skin-gradients="true"][data-interface-skin="roundcube-classic"] {
--skin-primary-bg: linear-gradient(color-mix(in srgb, var(--accent) 78%, white), var(--accent));
}
html[data-skin-gradients="true"][data-interface-skin="roundcube-classic"][data-theme="light"] {
--skin-appbar-bg: linear-gradient(#f4f4f4, #cccccc);
}
html[data-skin-gradients="true"][data-interface-skin="roundcube-classic"][data-theme="dark"] {
--skin-appbar-bg: linear-gradient(#3a3a3a, #292929);
}
html[data-interface-skin="roundcube-larry"] {
--skin-radius-control: 3px;
--skin-radius-panel: 4px;
--skin-radius-row: 3px;
--skin-radius-avatar: 4px;
--skin-shell-gap: 6px;
--skin-shell-padding: 6px;
--skin-pane-border: 1px solid var(--line-strong);
--skin-pane-shadow: 0 1px 3px var(--shadow);
--skin-appbar-fg: #ffffff;
--skin-search-bg: #ffffff;
--skin-search-fg: #333333;
--skin-search-border: transparent;
--skin-heading-tracking: 0;
--skin-focus-ring: 0 0 0 1px var(--accent);
}
html[data-interface-skin="roundcube-larry"][data-theme="light"] {
--skin-appbar-bg: #3c3c3c;
--skin-appbar-border: #202020;
}
html[data-interface-skin="roundcube-larry"][data-theme="dark"] {
--skin-appbar-bg: #171b1d;
--skin-appbar-border: #3b4449;
--skin-search-bg: #1d2225;
--skin-search-fg: #edf2f4;
}
html[data-interface-skin="roundcube-elastic"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-radius-control: 4px;
--skin-radius-row: 0px;
--skin-appbar-border: var(--line);
--skin-marker-width: 3px;
--skin-heading-weight: 600;
--skin-focus-ring: 0 0 0 .2rem color-mix(in srgb, var(--accent) 25%, transparent);
}
html[data-interface-skin="roundcube-elastic"][data-theme="light"] {
--skin-appbar-bg: #f4f4f4;
}
html[data-interface-skin="roundcube-elastic"][data-theme="dark"] {
--skin-appbar-bg: #2f3a3f;
}
html[data-interface-skin="clean"] {
--skin-row-inset: 6px;
--skin-row-gap: 2px;
--skin-row-divider: transparent;
--skin-radius-control: 10px;
--skin-radius-panel: 14px;
--skin-radius-row: 10px;
--skin-shell-gap: 10px;
--skin-shell-padding: 0 10px 10px;
--skin-pane-shadow: 0 1px 3px var(--shadow), 0 10px 28px -18px var(--shadow);
}
html[data-interface-skin="gmail"] {
--skin-font-heading: "Google Sans", "Product Sans", var(--font-sans);
--skin-heading-weight: 500;
--skin-heading-tracking: 0;
--skin-radius-panel: 16px;
--skin-radius-row: 0px;
--skin-folder-radius: 0 999px 999px 0;
--skin-sidebar-bg: transparent;
--skin-sidebar-border: 0 solid transparent;
--skin-sidebar-shadow: none;
--skin-pane-border: 0 solid transparent;
--skin-pane-shadow: none;
--skin-pane-divider: 1px solid var(--line);
--skin-shell-padding: 0 16px 16px 0;
--skin-search-bg: var(--panel-3);
--skin-search-border: transparent;
--skin-search-radius: 999px;
--skin-radius-compose: 16px;
--skin-compose-shadow: 0 1px 3px rgba(0, 0, 0, .18);
--skin-radius-button: 999px;
--skin-section-transform: none;
--skin-section-tracking: 0;
--skin-button-weight: 500;
}
html[data-interface-skin="gmail"][data-theme="light"] {
--skin-compose-bg: #c2e7ff;
--skin-compose-fg: #001d35;
}
html[data-interface-skin="gmail"][data-theme="dark"] {
--skin-compose-bg: color-mix(in srgb, var(--accent) 30%, var(--panel));
--skin-compose-fg: var(--text);
}
html[data-interface-skin="proton"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-radius-control: 8px;
--skin-radius-row: 8px;
--skin-appbar-bg: var(--sidebar);
--skin-marker-width: 3px;
--skin-section-transform: none;
--skin-section-tracking: 0;
}
html[data-interface-skin="zoho"] {
--skin-radius-control: 6px;
--skin-radius-panel: 8px;
--skin-radius-row: 6px;
--skin-shell-gap: 6px;
--skin-shell-padding: 0 6px 6px;
--skin-appbar-bg: var(--panel);
--skin-appbar-border: var(--line);
--skin-marker-width: 3px;
}
html[data-interface-skin="outlook"] {
--skin-font: "Segoe UI Variable Text", "Segoe UI", var(--font-sans);
--skin-font-heading: "Segoe UI Variable Display", "Segoe UI", var(--font-sans);
--skin-heading-weight: 600;
--skin-radius-control: 4px;
--skin-radius-panel: 8px;
--skin-radius-row: 4px;
--skin-appbar-bg: var(--accent);
--skin-appbar-fg: var(--accent-ink);
--skin-search-bg: var(--field);
--skin-search-border: transparent;
--skin-marker-width: 3px;
--skin-section-transform: none;
--skin-section-tracking: 0;
}
html[data-interface-skin="minimal"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-radius-control: 6px;
--skin-radius-row: 6px;
--skin-appbar-bg: var(--panel);
--skin-appbar-border: var(--line);
--skin-section-transform: none;
--skin-section-tracking: 0;
--skin-heading-weight: 600;
--skin-row-divider: transparent;
--skin-button-weight: 500;
--skin-control-border: var(--line);
}
html[data-interface-skin="graphite"] {
--skin-radius-control: 5px;
--skin-radius-panel: 8px;
--skin-radius-row: 5px;
--skin-shell-gap: 6px;
--skin-shell-padding: 0 6px 6px;
--skin-pane-border: 1px solid var(--line-strong);
--skin-appbar-bg: var(--panel-2);
--skin-appbar-border: var(--line-strong);
--skin-marker-width: 2px;
--skin-marker-color: var(--text);
--skin-heading-tracking: .01em;
--skin-section-tracking: .14em;
--skin-button-weight: 650;
}
html[data-interface-skin="high-contrast"] {
--skin-radius-control: 3px;
--skin-radius-panel: 0px;
--skin-radius-row: 0px;
--skin-radius-avatar: 0px;
--skin-shell-gap: 4px;
--skin-shell-padding: 0 4px 4px;
--skin-pane-border: 2px solid var(--line-strong);
--skin-pane-shadow: none;
--skin-appbar-bg: var(--panel);
--skin-appbar-border: var(--line-strong);
--skin-row-divider: var(--line-strong);
--skin-marker-width: 5px;
--skin-marker-color: var(--warning);
--skin-heading-weight: 800;
--skin-button-weight: 800;
--skin-search-border: var(--line-strong);
--skin-focus-ring: 0 0 0 3px var(--warning);
}
html[data-interface-skin="fastmail"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-radius-control: 6px;
--skin-radius-row: 6px;
--skin-appbar-bg: var(--panel);
--skin-appbar-border: var(--line);
--skin-marker-width: 3px;
--skin-section-transform: none;
--skin-section-tracking: 0;
}
html[data-interface-skin="apple-mail"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-row-inset: 8px;
--skin-row-gap: 1px;
--skin-row-divider: transparent;
--skin-font: -apple-system, BlinkMacSystemFont, "SF Pro Text", var(--font-sans);
--skin-font-heading: -apple-system, BlinkMacSystemFont, "SF Pro Display", var(--font-sans);
--skin-heading-weight: 700;
--skin-radius-control: 7px;
--skin-radius-row: 7px;
--skin-sidebar-bg: color-mix(in srgb, var(--sidebar) 82%, transparent);
--skin-glass: blur(20px) saturate(1.6);
--skin-appbar-bg: var(--panel-2);
--skin-appbar-border: var(--line);
--skin-selected-bg: var(--accent);
--skin-selected-fg: var(--accent-ink);
--skin-selected-muted: color-mix(in srgb, var(--accent-ink) 80%, transparent);
--skin-folder-active-bg: color-mix(in srgb, var(--text) 12%, transparent);
--skin-folder-active-fg: var(--text);
--skin-search-radius: 7px;
--skin-section-transform: none;
--skin-section-tracking: 0;
}
html[data-interface-skin="yahoo"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-appbar-bg: var(--panel);
--skin-appbar-border: var(--line);
--skin-search-bg: var(--panel-3);
--skin-search-border: transparent;
--skin-search-radius: 999px;
--skin-radius-button: 999px;
--skin-heading-weight: 700;
}
html[data-interface-skin="hey"] {
--skin-row-inset: 8px;
--skin-row-gap: 4px;
--skin-row-divider: transparent;
--skin-heading-weight: 800;
--skin-radius-control: 10px;
--skin-radius-panel: 16px;
--skin-radius-row: 12px;
--skin-shell-gap: 12px;
--skin-shell-padding: 0 12px 12px;
--skin-pane-border: 0 solid transparent;
--skin-pane-shadow: 0 2px 10px var(--shadow);
--skin-radius-button: 999px;
--skin-primary-shadow: 0 2px 0 color-mix(in srgb, var(--accent) 60%, black);
--skin-section-transform: none;
--skin-section-tracking: 0;
}
html[data-interface-skin="nord"] {
--skin-radius-control: 6px;
--skin-radius-panel: 10px;
--skin-radius-row: 6px;
--skin-marker-width: 3px;
--skin-heading-tracking: .01em;
}
html[data-interface-skin="github"] {
--skin-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
--skin-radius-control: 6px;
--skin-radius-panel: 6px;
--skin-radius-row: 6px;
--skin-shell-gap: 12px;
--skin-shell-padding: 0 12px 12px;
--skin-pane-shadow: none;
--skin-appbar-border: var(--line);
--skin-search-bg: color-mix(in srgb, var(--skin-appbar-fg) 8%, var(--skin-appbar-bg));
--skin-search-fg: var(--skin-appbar-fg);
--skin-search-border: color-mix(in srgb, var(--skin-appbar-fg) 28%, var(--skin-appbar-bg));
--skin-primary-fg: #ffffff;
--skin-marker-width: 2px;
--skin-marker-color: #fd8c73;
--skin-heading-weight: 600;
--skin-button-weight: 500;
--skin-section-transform: none;
--skin-section-tracking: 0;
}
html[data-interface-skin="github"][data-theme="light"] {
--skin-appbar-bg: #24292f;
--skin-appbar-fg: #ffffff;
--skin-primary-bg: #1f883d;
}
html[data-interface-skin="github"][data-theme="dark"] {
--skin-appbar-bg: #010409;
--skin-appbar-fg: #f0f6fc;
--skin-primary-bg: #238636;
}
html[data-interface-skin="gitlab"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-font: "GitLab Sans", var(--font-sans);
--skin-radius-control: 4px;
--skin-radius-row: 4px;
--skin-appbar-bg: var(--sidebar);
--skin-appbar-border: var(--line);
--skin-marker-width: 3px;
--skin-heading-weight: 600;
--skin-section-transform: none;
--skin-section-tracking: 0;
}
html[data-interface-skin="discord"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-row-inset: 8px;
--skin-row-gap: 2px;
--skin-row-divider: transparent;
--skin-radius-control: 4px;
--skin-radius-row: 4px;
--skin-sidebar-bg: var(--panel-2);
--skin-appbar-bg: var(--bg);
--skin-heading-weight: 700;
--skin-section-tracking: .02em;
}
html[data-interface-skin="slack"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-radius-control: 6px;
--skin-radius-row: 6px;
--skin-sidebar-accent: #ffffff;
--skin-sidebar-hover: color-mix(in srgb, white 9%, transparent);
--skin-folder-active-bg: #1164a3;
--skin-folder-active-fg: #ffffff;
--skin-appbar-fg: #ffffff;
--skin-search-bg: color-mix(in srgb, white 18%, var(--skin-appbar-bg));
--skin-search-fg: #ffffff;
--skin-search-border: transparent;
--skin-heading-weight: 800;
--skin-section-transform: none;
--skin-section-tracking: 0;
}
html[data-interface-skin="slack"][data-theme="light"] {
--skin-sidebar-bg: #3f0e40;
--skin-sidebar-fg: #ffffff;
--skin-sidebar-muted: #cfc3cf;
--skin-appbar-bg: #350d36;
--skin-compose-bg: #ffffff;
--skin-compose-fg: #3f0e40;
}
html[data-interface-skin="slack"][data-theme="dark"] {
--skin-sidebar-bg: #19171d;
--skin-sidebar-fg: #f8f8f8;
--skin-sidebar-muted: #b8b4bb;
--skin-appbar-bg: #1a1d21;
}
html[data-interface-skin="reddit"] {
--skin-row-inset: 8px;
--skin-row-gap: 4px;
--skin-row-divider: transparent;
--skin-radius-control: 999px;
--skin-radius-button: 999px;
--skin-radius-panel: 16px;
--skin-radius-row: 12px;
--skin-folder-radius: 8px;
--skin-search-bg: var(--panel-3);
--skin-search-border: transparent;
--skin-search-radius: 999px;
--skin-appbar-bg: var(--panel);
--skin-appbar-border: var(--line);
--skin-heading-weight: 700;
}
html[data-interface-skin="notion"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, sans-serif;
--skin-radius-control: 4px;
--skin-radius-row: 4px;
--skin-folder-radius: 4px;
--skin-appbar-bg: var(--panel);
--skin-row-divider: transparent;
--skin-section-transform: none;
--skin-section-tracking: 0;
--skin-heading-weight: 600;
--skin-button-weight: 500;
--skin-control-border: var(--line);
}
html[data-interface-skin="notion"][data-theme="light"] {
--skin-sidebar-bg: #f7f7f5;
}
html[data-interface-skin="notion"][data-theme="dark"] {
--skin-sidebar-bg: #202020;
}
html[data-interface-skin="dracula"] {
--skin-marker-width: 3px;
--skin-heading-tracking: 0;
}
html[data-interface-skin="dracula"][data-theme="light"] {
--skin-marker-color: #c4337f;
--skin-primary-bg: var(--accent);
--skin-primary-fg: #ffffff;
}
html[data-interface-skin="dracula"][data-theme="dark"] {
--skin-marker-color: #ff79c6;
--skin-primary-bg: var(--accent);
--skin-primary-fg: #1d1530;
}
html[data-skin-gradients="true"][data-interface-skin="dracula"][data-theme="light"] {
--skin-primary-bg: linear-gradient(135deg, #644ac9, #a3144d);
}
html[data-skin-gradients="true"][data-interface-skin="dracula"][data-theme="dark"] {
--skin-primary-bg: linear-gradient(135deg, #bd93f9, #ff79c6);
}
html[data-interface-skin="catppuccin"] {
--skin-row-inset: 6px;
--skin-row-gap: 2px;
--skin-row-divider: transparent;
--skin-radius-control: 10px;
--skin-radius-panel: 16px;
--skin-radius-row: 10px;
--skin-radius-button: 999px;
--skin-shell-gap: 10px;
--skin-shell-padding: 0 10px 10px;
--skin-pane-shadow: none;
--skin-section-transform: none;
--skin-section-tracking: .02em;
}
html[data-interface-skin="solarized"] {
--skin-radius-control: 4px;
--skin-radius-panel: 6px;
--skin-radius-row: 4px;
--skin-shell-gap: 6px;
--skin-shell-padding: 0 6px 6px;
--skin-pane-shadow: none;
--skin-marker-width: 3px;
--skin-marker-color: #b58900;
--skin-font-heading: var(--font-mono);
--skin-heading-weight: 600;
--skin-heading-tracking: 0;
}
html[data-interface-skin="gruvbox"] {
--skin-radius-control: 2px;
--skin-radius-panel: 2px;
--skin-radius-row: 2px;
--skin-radius-avatar: 2px;
--skin-shell-gap: 6px;
--skin-shell-padding: 0 6px 6px;
--skin-pane-border: 1px solid var(--line-strong);
--skin-pane-shadow: none;
--skin-marker-width: 3px;
--skin-marker-color: var(--warning);
--skin-font-heading: var(--font-mono);
--skin-heading-weight: 700;
--skin-heading-tracking: 0;
--skin-section-tracking: .12em;
}
html[data-interface-skin="tokyo-night"] {
--skin-appbar-bg: transparent;
--skin-marker-width: 2px;
}
html[data-interface-skin="tokyo-night"][data-theme="light"] {
--skin-backdrop: var(--bg);
--skin-pane-shadow: 0 10px 28px -16px rgba(55, 96, 191, .35);
--skin-marker-color: #9854f1;
--skin-primary-bg: var(--accent);
--skin-primary-fg: #ffffff;
}
html[data-interface-skin="tokyo-night"][data-theme="dark"] {
--skin-backdrop: var(--bg);
--skin-pane-shadow: 0 10px 28px -14px rgba(0, 0, 0, .7);
--skin-marker-color: #bb9af7;
--skin-primary-bg: var(--accent);
}
html[data-skin-gradients="true"][data-interface-skin="tokyo-night"][data-theme="light"] {
--skin-backdrop: radial-gradient(1200px 600px at 85% -10%, rgba(46, 125, 233, .14), transparent 60%), var(--bg);
--skin-primary-bg: linear-gradient(135deg, var(--accent), #9854f1);
}
html[data-skin-gradients="true"][data-interface-skin="tokyo-night"][data-theme="dark"] {
--skin-backdrop: radial-gradient(1200px 600px at 85% -10%, rgba(122, 162, 247, .12), transparent 60%), var(--bg);
--skin-primary-bg: linear-gradient(135deg, var(--accent), #bb9af7);
}
html[data-interface-skin="rose-pine"] {
--skin-row-inset: 6px;
--skin-row-gap: 2px;
--skin-row-divider: transparent;
--skin-radius-control: 10px;
--skin-radius-panel: 14px;
--skin-radius-row: 10px;
--skin-radius-button: 999px;
--skin-shell-gap: 10px;
--skin-shell-padding: 0 10px 10px;
--skin-pane-shadow: none;
--skin-section-transform: none;
--skin-section-tracking: .01em;
}
html[data-interface-skin="everforest"] {
--skin-radius-control: 8px;
--skin-radius-panel: 12px;
--skin-pane-shadow: none;
--skin-marker-width: 3px;
--skin-section-tracking: .06em;
}
html[data-interface-skin="synthwave"] {
--skin-font-heading: var(--font-mono);
--skin-heading-transform: uppercase;
--skin-heading-tracking: .12em;
--skin-heading-weight: 800;
--skin-radius-control: 6px;
--skin-radius-panel: 10px;
--skin-radius-row: 6px;
--skin-appbar-bg: transparent;
--skin-appbar-border: color-mix(in srgb, var(--accent) 30%, transparent);
--skin-pane-border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line));
--skin-marker-width: 3px;
--skin-section-tracking: .16em;
}
html[data-interface-skin="synthwave"][data-theme="light"] {
--skin-backdrop: var(--bg);
--skin-pane-shadow: 0 10px 26px -16px rgba(214, 36, 159, .45);
--skin-primary-bg: var(--accent);
--skin-primary-fg: #ffffff;
--skin-marker-color: #0f8a64;
}
html[data-interface-skin="synthwave"][data-theme="dark"] {
--skin-backdrop: var(--bg);
--skin-pane-shadow: 0 0 0 1px rgba(255, 126, 219, .08), 0 0 26px -8px rgba(255, 126, 219, .38);
--skin-primary-bg: var(--accent);
--skin-primary-fg: #2a0d23;
--skin-primary-shadow: 0 0 14px rgba(255, 126, 219, .55);
--skin-marker-color: #72f1b8;
}
html[data-skin-gradients="true"][data-interface-skin="synthwave"][data-theme="light"] {
--skin-backdrop: linear-gradient(180deg, #f3e9ff 0%, #ffe3f3 100%);
--skin-primary-bg: linear-gradient(90deg, #d6249f, #7c3aed);
}
html[data-skin-gradients="true"][data-interface-skin="synthwave"][data-theme="dark"] {
--skin-backdrop: linear-gradient(180deg, #1a1029 0%, #241038 55%, #3a1145 100%);
--skin-primary-bg: linear-gradient(90deg, #ff7edb, #f97e72);
}
html[data-interface-skin="paper"] {
--skin-shell-gap: 0px;
--skin-shell-padding: 0;
--skin-pane-border: 0 solid transparent;
--skin-pane-divider: 1px solid var(--line);
--skin-pane-shadow: none;
--skin-radius-panel: 0px;
--skin-font-heading: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Charter, Georgia, serif;
--skin-heading-weight: 600;
--skin-heading-tracking: 0;
--skin-section-transform: none;
--skin-section-variant: all-small-caps;
--skin-section-tracking: .06em;
--skin-radius-control: 3px;
--skin-radius-row: 0px;
--skin-appbar-bg: var(--panel);
--skin-appbar-border: var(--line-strong);
--skin-appbar-shadow: 0 3px 0 -2px var(--line);
--skin-button-weight: 500;
}
html[data-interface-skin="terminal"] {
--skin-font: var(--font-mono);
--skin-font-heading: var(--font-mono);
--skin-heading-transform: uppercase;
--skin-heading-tracking: .06em;
--skin-radius-control: 0px;
--skin-radius-panel: 0px;
--skin-radius-row: 0px;
--skin-radius-avatar: 0px;
--skin-shell-gap: 6px;
--skin-shell-padding: 0 6px 6px;
--skin-pane-border: 1px solid var(--line-strong);
--skin-marker-width: 2px;
--skin-primary-bg: transparent;
--skin-primary-fg: var(--accent);
--skin-primary-shadow: inset 0 0 0 1px var(--accent);
--skin-focus-ring: 0 0 0 1px var(--accent);
}
html[data-interface-skin="terminal"][data-theme="light"] {
--skin-pane-shadow: none;
}
html[data-interface-skin="terminal"][data-theme="dark"] {
--skin-pane-shadow: 0 0 0 1px rgba(57, 255, 106, .06), 0 0 18px -8px rgba(57, 255, 106, .35);
--skin-backdrop: var(--bg);
}
html[data-skin-gradients="true"][data-interface-skin="terminal"][data-theme="dark"] {
--skin-backdrop: repeating-linear-gradient(0deg, rgba(57, 255, 106, .035) 0 1px, transparent 1px 3px), var(--bg);
}
html[data-interface-skin="aqua"] {
--skin-row-inset: 6px;
--skin-row-gap: 2px;
--skin-row-divider: transparent;
--skin-glass: blur(22px) saturate(1.5);
--skin-pane-bg: color-mix(in srgb, var(--panel) 72%, transparent);
--skin-reader-bg: color-mix(in srgb, var(--panel) 84%, transparent);
--skin-sidebar-bg: color-mix(in srgb, var(--sidebar) 56%, transparent);
--skin-pane-shadow: 0 12px 32px -14px var(--shadow);
--skin-radius-control: 10px;
--skin-radius-panel: 16px;
--skin-radius-row: 10px;
--skin-shell-gap: 10px;
--skin-shell-padding: 0 10px 10px;
--skin-appbar-bg: transparent;
--skin-search-bg: color-mix(in srgb, var(--field) 70%, transparent);
--skin-search-radius: 999px;
--skin-primary-bg: var(--accent);
--skin-primary-shadow: inset 0 1px 0 rgba(255, 255, 255, .4), 0 4px 12px -4px var(--accent);
--skin-selected-bg: var(--accent);
--skin-selected-fg: #ffffff;
--skin-selected-muted: rgba(255, 255, 255, .82);
--skin-folder-active-bg: color-mix(in srgb, var(--accent) 22%, transparent);
--skin-folder-active-fg: var(--text);
}
html[data-interface-skin="aqua"][data-theme="light"] {
--skin-backdrop: var(--bg);
--skin-pane-border: 1px solid rgba(255, 255, 255, .6);
}
html[data-interface-skin="aqua"][data-theme="dark"] {
--skin-backdrop: var(--bg);
--skin-pane-border: 1px solid rgba(255, 255, 255, .08);
}
html[data-skin-gradients="true"][data-interface-skin="aqua"] {
--skin-primary-bg: linear-gradient(180deg, color-mix(in srgb, var(--accent) 72%, white), var(--accent));
}
html[data-skin-gradients="true"][data-interface-skin="aqua"][data-theme="light"] {
--skin-backdrop: radial-gradient(900px 520px at 8% 0%, #bfe0ff 0%, transparent 60%), radial-gradient(900px 620px at 100% 100%, #e8d6ff 0%, transparent 55%), linear-gradient(135deg, #e6eef8, #f4f0fb);
}
html[data-skin-gradients="true"][data-interface-skin="aqua"][data-theme="dark"] {
--skin-backdrop: radial-gradient(900px 520px at 8% 0%, rgba(74, 163, 255, .28), transparent 60%), radial-gradient(900px 620px at 100% 100%, rgba(175, 82, 222, .22), transparent 55%), #0b1220;
}
html[data-interface-skin="linear"] {
--skin-font: "Inter Variable", Inter, var(--font-sans);
--skin-heading-weight: 590;
--skin-heading-tracking: -0.012em;
--skin-radius-control: 6px;
--skin-radius-panel: 10px;
--skin-radius-row: 6px;
--skin-sidebar-bg: transparent;
--skin-sidebar-border: 0 solid transparent;
--skin-sidebar-shadow: none;
--skin-row-divider: transparent;
--skin-button-weight: 510;
--skin-control-border: var(--line);
--skin-selected-bg: color-mix(in srgb, var(--accent) 14%, var(--panel));
--skin-section-transform: none;
--skin-section-tracking: 0;
}
html[data-interface-skin="linear"][data-theme="light"] {
--skin-pane-shadow: 0 1px 2px rgba(0, 0, 0, .04);
}
html[data-interface-skin="linear"][data-theme="dark"] {
--skin-pane-shadow: none;
}
html[data-interface-skin="material"] {
--skin-row-inset: 8px;
--skin-row-gap: 2px;
--skin-row-divider: transparent;
--skin-radius-row: 12px;
--skin-heading-weight: 500;
--skin-heading-tracking: 0;
--skin-button-weight: 500;
--skin-radius-control: 12px;
--skin-radius-button: 999px;
--skin-radius-panel: 16px;
--skin-folder-radius: 999px;
--skin-pane-border: 0 solid transparent;
--skin-pane-shadow: none;
--skin-sidebar-bg: var(--bg);
--skin-search-bg: var(--panel-3);
--skin-search-border: transparent;
--skin-search-radius: 999px;
--skin-folder-active-bg: color-mix(in srgb, var(--accent) 24%, var(--panel));
--skin-folder-active-fg: var(--text);
--skin-radius-compose: 16px;
--skin-compose-shadow: 0 1px 3px rgba(0, 0, 0, .3), 0 4px 8px 3px rgba(0, 0, 0, .15);
--skin-section-transform: none;
--skin-section-tracking: .01em;
}
html[data-interface-skin="material"][data-theme="light"] {
--skin-compose-bg: #eaddff;
--skin-compose-fg: #21005d;
}
html[data-interface-skin="material"][data-theme="dark"] {
--skin-compose-bg: #4f378b;
--skin-compose-fg: #eaddff;
}
html[data-interface-skin="retro"] {
--skin-font: "MS Sans Serif", "Microsoft Sans Serif", Tahoma, "Segoe UI", sans-serif;
--skin-heading-weight: 700;
--skin-heading-tracking: 0;
--skin-radius-control: 0px;
--skin-radius-panel: 0px;
--skin-radius-row: 0px;
--skin-radius-avatar: 0px;
--skin-shell-gap: 4px;
--skin-shell-padding: 0 4px 4px;
--skin-pane-border: 0 solid transparent;
--skin-control-border: transparent;
--skin-primary-bg: var(--panel-3);
--skin-primary-fg: var(--text);
--skin-appbar-fg: #ffffff;
--skin-search-bg: var(--field);
--skin-search-fg: var(--text);
--skin-search-border: transparent;
--skin-search-radius: 0px;
--skin-selected-fg: #ffffff;
--skin-selected-muted: #dcdcff;
--skin-row-divider: transparent;
--skin-section-transform: none;
--skin-section-tracking: 0;
--skin-focus-ring: 0 0 0 1px var(--text);
}
html[data-interface-skin="retro"][data-theme="light"] {
--skin-backdrop: #008080;
--skin-pane-shadow: inset -1px -1px 0 #0a0a0a, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf;
--skin-control-shadow: inset -1px -1px 0 #0a0a0a, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf;
--skin-field-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff, inset 2px 2px 0 #0a0a0a, inset -2px -2px 0 #dfdfdf;
--skin-primary-shadow: 0 0 0 1px #000000, inset -1px -1px 0 #0a0a0a, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf;
--skin-appbar-bg: #000080;
--skin-selected-bg: #000080;
}
html[data-interface-skin="retro"][data-theme="dark"] {
--skin-backdrop: #0b2b2b;
--skin-pane-shadow: inset -1px -1px 0 #000000, inset 1px 1px 0 #5a5a62, inset -2px -2px 0 #111114, inset 2px 2px 0 #3c3c44;
--skin-control-shadow: inset -1px -1px 0 #000000, inset 1px 1px 0 #5a5a62, inset -2px -2px 0 #111114, inset 2px 2px 0 #3c3c44;
--skin-field-shadow: inset 1px 1px 0 #111114, inset -1px -1px 0 #5a5a62, inset 2px 2px 0 #000000, inset -2px -2px 0 #3c3c44;
--skin-primary-shadow: 0 0 0 1px #000000, inset -1px -1px 0 #000000, inset 1px 1px 0 #5a5a62, inset -2px -2px 0 #111114, inset 2px 2px 0 #3c3c44;
--skin-appbar-bg: #1c2f63;
--skin-selected-bg: #2b3f6b;
}
html[data-skin-gradients="true"][data-interface-skin="retro"][data-theme="light"] {
--skin-appbar-bg: linear-gradient(90deg, #000080, #1084d0);
}
html[data-skin-gradients="true"][data-interface-skin="retro"][data-theme="dark"] {
--skin-appbar-bg: linear-gradient(90deg, #1c2f63, #3a6fd8);
}
+307
View File
@@ -0,0 +1,307 @@
/*
* "Immersive" style depth: applies the structural tokens from 57-skin-tokens.css.
*
* Every rule is scoped to :root[data-skin-depth="immersive"][data-theme] so it
* outranks the per-style chrome rules (html[data-interface-skin=…][data-theme=…] …)
* without !important, and has no effect at the "colors" or "standard" depth.
* Inline custom properties written by applyPreferences() live on <html>, so the
* shared radius/font variables are re-pointed on <body> where they inherit down.
*/
:root[data-skin-depth="immersive"][data-theme] body {
--font-sans: var(--skin-font);
--radius-control: var(--skin-radius-control);
--radius-panel: var(--skin-radius-panel);
font-family: var(--skin-font);
}
/* Backdrop and app bar ---------------------------------------------------- */
:root[data-skin-depth="immersive"][data-theme] .app-view {
background: var(--skin-backdrop);
}
:root[data-skin-depth="immersive"][data-theme] .appbar {
background: var(--skin-appbar-bg);
color: var(--skin-appbar-fg);
border-bottom-color: var(--skin-appbar-border);
box-shadow: var(--skin-appbar-shadow);
-webkit-backdrop-filter: var(--skin-glass);
backdrop-filter: var(--skin-glass);
}
:root[data-skin-depth="immersive"][data-theme] .appbar .icon-button,
:root[data-skin-depth="immersive"][data-theme] .appbar .account-summary,
:root[data-skin-depth="immersive"][data-theme] .appbar .brand-icon {
color: inherit;
}
:root[data-skin-depth="immersive"][data-theme] .appbar .account-summary {
border-left-color: color-mix(in srgb, currentColor 22%, transparent);
}
:root[data-skin-depth="immersive"][data-theme] .appbar .icon-button:hover {
background: color-mix(in srgb, currentColor 12%, transparent);
}
:root[data-skin-depth="immersive"][data-theme] .app-brand strong,
:root[data-skin-depth="immersive"][data-theme] .pane-title strong,
:root[data-skin-depth="immersive"][data-theme] #reader-subject,
:root[data-skin-depth="immersive"][data-theme] .dialog-header strong,
:root[data-skin-depth="immersive"][data-theme] .settings-panel-heading h2,
:root[data-skin-depth="immersive"][data-theme] .reader-empty strong {
font-family: var(--skin-font-heading);
font-weight: var(--skin-heading-weight);
letter-spacing: var(--skin-heading-tracking);
text-transform: var(--skin-heading-transform);
}
:root[data-skin-depth="immersive"][data-theme] .search-field {
border-color: var(--skin-search-border);
border-radius: var(--skin-search-radius);
background: var(--skin-search-bg);
color: var(--skin-search-fg);
box-shadow: var(--skin-field-shadow);
}
:root[data-skin-depth="immersive"][data-theme] .search-field input,
:root[data-skin-depth="immersive"][data-theme] .search-field svg,
:root[data-skin-depth="immersive"][data-theme] .search-field .search-filter-button,
:root[data-skin-depth="immersive"][data-theme] .search-field kbd {
color: inherit;
}
:root[data-skin-depth="immersive"][data-theme] .search-field input::placeholder {
color: color-mix(in srgb, currentColor 62%, transparent);
}
/* Panes ------------------------------------------------------------------- */
:root[data-skin-depth="immersive"][data-theme] .folder-pane {
background: var(--skin-sidebar-bg);
color: var(--skin-sidebar-fg);
-webkit-backdrop-filter: var(--skin-glass);
backdrop-filter: var(--skin-glass);
}
:root[data-skin-depth="immersive"][data-theme] .message-pane {
background: var(--skin-pane-bg);
-webkit-backdrop-filter: var(--skin-glass);
backdrop-filter: var(--skin-glass);
}
:root[data-skin-depth="immersive"][data-theme] .reader-pane,
:root[data-skin-depth="immersive"][data-theme] .reader-content {
background: var(--skin-reader-bg);
}
:root[data-skin-depth="immersive"][data-theme] .reader-pane {
-webkit-backdrop-filter: var(--skin-glass);
backdrop-filter: var(--skin-glass);
}
/* Floating panes only where the three-pane desktop layout is shown; the
phone/tablet drawer layout in 60-responsive.css stays untouched. */
@media (min-width: 821px) {
:root[data-skin-depth="immersive"][data-theme] .mail-shell {
gap: var(--skin-shell-gap);
padding: var(--skin-shell-padding);
}
:root[data-skin-depth="immersive"][data-theme] .folder-pane,
:root[data-skin-depth="immersive"][data-theme] .message-pane,
:root[data-skin-depth="immersive"][data-theme] .reader-pane {
border-radius: var(--skin-radius-panel);
}
:root[data-skin-depth="immersive"][data-theme] .message-pane,
:root[data-skin-depth="immersive"][data-theme] .reader-pane {
border: var(--skin-pane-border);
box-shadow: var(--skin-pane-shadow);
}
:root[data-skin-depth="immersive"][data-theme] .message-pane {
border-right: var(--skin-pane-divider);
}
:root[data-skin-depth="immersive"][data-theme] .folder-pane {
border: var(--skin-sidebar-border);
border-right: var(--skin-pane-divider);
box-shadow: var(--skin-sidebar-shadow);
}
}
/* Folder list ------------------------------------------------------------- */
:root[data-skin-depth="immersive"][data-theme] .section-heading,
:root[data-skin-depth="immersive"][data-theme] .folder-quota-label,
:root[data-skin-depth="immersive"][data-theme] .folder-pane-footer {
color: var(--skin-sidebar-muted);
}
:root[data-skin-depth="immersive"][data-theme] .section-heading {
font-family: var(--skin-font-heading);
font-variant-caps: var(--skin-section-variant);
letter-spacing: var(--skin-section-tracking);
text-transform: var(--skin-section-transform);
}
:root[data-skin-depth="immersive"][data-theme] .folder-pane .icon-button {
color: var(--skin-sidebar-muted);
}
:root[data-skin-depth="immersive"][data-theme] .tag-item {
border-radius: var(--skin-folder-radius);
color: var(--skin-sidebar-muted);
}
:root[data-skin-depth="immersive"][data-theme] :is(.tag-item:hover, .tag-item.active) {
background: var(--skin-sidebar-hover);
color: var(--skin-sidebar-fg);
}
:root[data-skin-depth="immersive"][data-theme] .folder-pane :is(.folder-count, .tag-count) {
color: var(--skin-sidebar-muted);
}
:root[data-skin-depth="immersive"][data-theme] .folder-pane :is(.folder-count .folder-unread, .folder-count.unread .folder-count-compact) {
color: var(--skin-sidebar-accent);
}
:root[data-skin-depth="immersive"][data-theme] .folder-button {
border-color: transparent;
border-radius: var(--skin-folder-radius);
color: var(--skin-sidebar-muted);
}
:root[data-skin-depth="immersive"][data-theme] .folder-button:hover {
background: var(--skin-sidebar-hover);
color: var(--skin-sidebar-fg);
}
:root[data-skin-depth="immersive"][data-theme] .folder-button.active {
border-color: transparent;
background: var(--skin-folder-active-bg);
color: var(--skin-folder-active-fg);
box-shadow: inset var(--skin-marker-width) 0 0 var(--skin-marker-color);
}
:root[data-skin-depth="immersive"][data-theme] .folder-button.active .folder-count,
:root[data-skin-depth="immersive"][data-theme] .folder-button.active .folder-count .folder-unread {
color: inherit;
}
/* Message rows ------------------------------------------------------------ */
:root[data-skin-depth="immersive"][data-theme] .message-row {
width: calc(100% - (2 * var(--skin-row-inset)));
margin: 0 var(--skin-row-inset) var(--skin-row-gap);
border-bottom-color: var(--skin-row-divider);
border-radius: min(calc(var(--skin-row-inset) * 100), var(--skin-radius-row));
background: transparent;
}
:root[data-skin-depth="immersive"][data-theme] .message-list {
padding-top: var(--skin-row-inset);
}
:root[data-skin-depth="immersive"][data-theme] .message-row:hover {
background: var(--hover);
}
:root[data-skin-depth="immersive"][data-theme] .message-row.active {
background: var(--skin-selected-bg);
color: var(--skin-selected-fg);
box-shadow: inset var(--skin-marker-width) 0 0 var(--skin-marker-color);
}
:root[data-skin-depth="immersive"][data-theme] .message-row.active :is(.message-from, .message-subject) {
color: var(--skin-selected-fg);
}
:root[data-skin-depth="immersive"][data-theme] .message-row.active :is(.message-snippet, .message-date, .message-icons) {
color: var(--skin-selected-muted);
}
:root[data-skin-depth="immersive"][data-theme] .message-row.active .message-state {
border-color: currentColor;
}
/* Controls ---------------------------------------------------------------- */
:root[data-skin-depth="immersive"][data-theme] .button {
border-color: var(--skin-control-border);
border-radius: var(--skin-radius-button);
box-shadow: var(--skin-control-shadow);
font-weight: var(--skin-button-weight);
}
:root[data-skin-depth="immersive"][data-theme] .button.primary {
border-color: transparent;
background: var(--skin-primary-bg);
color: var(--skin-primary-fg);
box-shadow: var(--skin-primary-shadow);
}
:root[data-skin-depth="immersive"][data-theme] .button.primary:hover {
filter: brightness(1.06) saturate(1.04);
}
:root[data-skin-depth="immersive"][data-theme] .button.compose-button {
min-height: calc(var(--control-height) + 8px);
border-radius: var(--skin-radius-compose);
background: var(--skin-compose-bg);
color: var(--skin-compose-fg);
box-shadow: var(--skin-compose-shadow);
}
:root[data-skin-depth="immersive"][data-theme] :is(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]), select, textarea) {
box-shadow: var(--skin-field-shadow);
}
:root[data-skin-depth="immersive"][data-theme] :is(input, select, textarea, [contenteditable="true"]):focus,
:root[data-skin-depth="immersive"][data-theme] .search-field:focus-within {
border-color: var(--accent);
box-shadow: var(--skin-focus-ring);
outline: none;
}
:root[data-skin-depth="immersive"][data-theme] .avatar {
border-radius: var(--skin-radius-avatar);
}
:root[data-skin-depth="immersive"][data-theme] .dialog {
border-radius: var(--skin-radius-panel);
}
/* Style-specific finishing touches that cannot be expressed as a token. -- */
:root[data-skin-depth="immersive"][data-interface-skin="terminal"][data-theme="dark"] :is(.app-brand strong, .pane-title strong, .folder-button.active, .message-row.unread .message-subject) {
text-shadow: 0 0 6px color-mix(in srgb, var(--accent) 55%, transparent);
}
:root[data-skin-depth="immersive"][data-skin-gradients="true"][data-interface-skin="synthwave"][data-theme="dark"] .app-brand strong {
background: linear-gradient(90deg, #ff7edb, #fede5d);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
@media (prefers-reduced-transparency: reduce) {
:root[data-skin-depth="immersive"][data-theme] :is(.appbar, .folder-pane, .message-pane, .reader-pane) {
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
}
/* Floating panes are a little narrower than the flush layout, so drop the
reader toolbar's button labels a bit earlier than the standard 640px. */
@container reader-pane (max-width: 820px) {
:root[data-skin-depth="immersive"][data-theme] .reader-toolbar .button span {
display: none;
}
:root[data-skin-depth="immersive"][data-theme] .reader-toolbar .button {
padding-inline: 6px;
}
}
+2
View File
@@ -6,4 +6,6 @@
50-settings.css
55-roundcube-skins.css
56-interface-styles.css
57-skin-tokens.css
58-skin-immersive.css
60-responsive.css
+10 -1
View File
@@ -59,5 +59,14 @@
"copy_to": "Copy to…",
"login_success": "Successfully logged in",
"adaptive_email_colors": "Adapt HTML mail colors to current theme",
"adaptive_email_colors_note": "Adjusts neutral backgrounds and low-contrast text while preserving layout, images and brand colors."
"adaptive_email_colors_note": "Adjusts neutral backgrounds and low-contrast text while preserving layout, images and brand colors.",
"skin_depth": "Style depth",
"skin_depth_colors": "Colors only",
"skin_depth_standard": "Standard",
"skin_depth_immersive": "Immersive",
"skin_depth_colors_note": "Only the style's colors are used. Layout, spacing and typography follow your own settings below.",
"skin_depth_standard_note": "Colors plus the style's layout: density, row height, pane widths and type size.",
"skin_depth_immersive_note": "Everything in Standard plus the style's shapes, typography, floating panes, app bar, selection markers and backdrop.",
"skin_gradients": "Style gradients",
"skin_gradients_note": "Allow predefined styles to use gradients (backdrops, app bars, buttons). When off, every style uses flat colors."
}
+18 -2
View File
@@ -657,7 +657,14 @@
"Use single-line rows when the message list is wide": "Używaj jednowierszowej listy, gdy panel wiadomości jest szeroki",
"Signature insertion": "Wstawianie podpisu",
"Quietly when sending": "Cicho przy wysyłce",
"Show in composer": "Pokaż w edytorze"
"Show in composer": "Pokaż w edytorze",
"Linear inspired": "Inspirowany Linear",
"Material You inspired": "Inspirowany Material You",
"Aqua Glass": "Szkło Aqua",
"Paper": "Papier",
"Terminal": "Terminal",
"Retro 95": "Retro 95",
"Style depth": "Głębokość stylu"
},
"fixed_interface_style_note": "gotowy styl interfejsu. Włącz edycję presetu poniżej, aby przy pierwszej zmianie utworzyć jego edytowalną kopię.",
"custom_interface_style_note": "RustBox można w pełni dostosować: kolory, odstępy, panele, typografię i zaawansowane ustawienia interfejsu.",
@@ -671,5 +678,14 @@
"copy_to": "Kopiuj do…",
"login_success": "Pomyślnie zalogowano",
"adaptive_email_colors": "Dopasuj kolory maili HTML do bieżącego motywu",
"adaptive_email_colors_note": "Dostosowuje neutralne tła i tekst o niskim kontraście bez zmiany układu, obrazów i kolorów firmowych."
"adaptive_email_colors_note": "Dostosowuje neutralne tła i tekst o niskim kontraście bez zmiany układu, obrazów i kolorów firmowych.",
"skin_depth": "Głębokość stylu",
"skin_depth_colors": "Tylko kolory",
"skin_depth_standard": "Standardowa",
"skin_depth_immersive": "Pełna (immersyjna)",
"skin_depth_colors_note": "Używane są tylko kolory stylu. Układ, odstępy i typografia pochodzą z Twoich ustawień poniżej.",
"skin_depth_standard_note": "Kolory oraz układ stylu: gęstość, wysokość wierszy, szerokości paneli i rozmiar tekstu.",
"skin_depth_immersive_note": "Wszystko ze standardowej oraz kształty, typografia, pływające panele, pasek aplikacji, znaczniki zaznaczenia i tło stylu.",
"skin_gradients": "Gradienty w stylach",
"skin_gradients_note": "Pozwala gotowym stylom używać gradientów (tła, paski aplikacji, przyciski). Po wyłączeniu każdy styl używa jednolitych kolorów."
}
+24
View File
@@ -655,8 +655,32 @@
<option value="slack">Slack inspired</option>
<option value="reddit">Reddit inspired</option>
<option value="notion">Notion inspired</option>
<option value="dracula">Dracula</option>
<option value="catppuccin">Catppuccin</option>
<option value="solarized">Solarized</option>
<option value="gruvbox">Gruvbox</option>
<option value="tokyo-night">Tokyo Night</option>
<option value="rose-pine">Rosé Pine</option>
<option value="everforest">Everforest</option>
<option value="synthwave">Synthwave</option>
<option value="paper">Paper</option>
<option value="terminal">Terminal</option>
<option value="aqua">Aqua Glass</option>
<option value="linear">Linear inspired</option>
<option value="material">Material You inspired</option>
<option value="retro">Retro 95</option>
</select><small id="pref-interface-skin-note" class="muted">RustBox is fully customizable: colors,
spacing, panes and typography.</small></label>
<label><span data-i18n="skin_depth">Style depth</span><select id="pref-skin-depth">
<option value="colors" data-i18n="skin_depth_colors">Colors only</option>
<option value="standard" data-i18n="skin_depth_standard">Standard</option>
<option value="immersive" data-i18n="skin_depth_immersive">Immersive</option>
</select><small id="pref-skin-depth-note" class="muted">Colors plus the style's layout: density, row
height, pane widths and type size.</small></label>
<label id="pref-skin-gradients-row" class="checkbox-row"><input id="pref-skin-gradients"
type="checkbox"><span data-i18n="skin_gradients">Style gradients</span><small class="muted"
data-i18n="skin_gradients_note">Allow predefined styles to use gradients (backdrops, app bars,
buttons). When off, every style uses flat colors.</small></label>
<label id="pref-preset-scale-unlocked-row" class="checkbox-row hidden"><input
id="pref-preset-scale-unlocked" type="checkbox"><span>Allow editing predefined style</span><small
class="muted">When enabled, the first appearance change creates and saves a RustBox custom copy of the
+1 -1
View File
@@ -102,7 +102,7 @@
imageEditorBlob: null,
draftSaveTimer: null,
prefs: {
theme: root.dataset.defaultTheme || "system", interfaceSkin: "default", presetScaleUnlocked: false, density: "compact", preview: "right", scale: "normal", language: "en", composeFormat: "html",
theme: root.dataset.defaultTheme || "system", interfaceSkin: "default", skinDepth: "standard", skinGradients: false, 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,
+120 -26
View File
@@ -13,7 +13,7 @@ const preferenceMap = {
composeAutosaveMs: "compose_autosave_ms", defaultPriority: "default_priority", requestMdnDefault: "request_mdn_default", attachmentReminder: "attachment_reminder", composeFontFamily: "compose_font_family", replyPosition: "reply_position", quoteStyle: "quote_style", forwardMode: "forward_mode", spellcheckEnabled: "spellcheck_enabled",
dateFormat: "date_format", timeFormat: "time_format", iconSize: "icon_size", toolbarDensity: "toolbar_density", sidebarItemHeight: "sidebar_item_height",
readerMaxWidth: "reader_max_width", composerWidth: "composer_width", selectionColor: "selection_color", unreadFontWeight: "unread_font_weight", unreadBadgeStyle: "unread_badge_style",
compactHeaders: "compact_headers", showBorders: "show_borders", scrollbarDensity: "scrollbar_density", monochrome: "monochrome", folderBackground: "folder_background", messageBackground: "message_background", readerBackground: "reader_background", adaptiveEmailColors: "adaptive_email_colors",
compactHeaders: "compact_headers", showBorders: "show_borders", scrollbarDensity: "scrollbar_density", monochrome: "monochrome", folderBackground: "folder_background", messageBackground: "message_background", readerBackground: "reader_background", adaptiveEmailColors: "adaptive_email_colors", skinDepth: "skin_depth", skinGradients: "skin_gradients",
};
const preferenceTimers = new Map();
let presetPromotionTimer = null;
@@ -111,6 +111,62 @@ const interfaceStyleProfiles = {
label: "Notion inspired",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 14, rowHeight: 58, folderPaneWidth: 238, messagePaneWidth: 430, borderRadius: 5, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 900, composerWidth: 920, selectionColor: "#efefef", unreadFontWeight: 700, unreadBadgeStyle: "outline", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
dracula: {
label: "Dracula",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 54, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 8, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 31, readerMaxWidth: 960, composerWidth: 940, selectionColor: "#44475a", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
catppuccin: {
label: "Catppuccin",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 58, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 10, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 33, readerMaxWidth: 960, composerWidth: 940, selectionColor: "#3a3350", unreadFontWeight: 720, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
solarized: {
label: "Solarized",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 52, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 4, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 29, readerMaxWidth: 960, composerWidth: 940, selectionColor: "#0d4a5a", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
gruvbox: {
label: "Gruvbox",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 50, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 2, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 28, readerMaxWidth: 960, composerWidth: 940, selectionColor: "#504945", unreadFontWeight: 760, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
"tokyo-night": {
label: "Tokyo Night",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 8, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 960, composerWidth: 940, selectionColor: "#2e3c64", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
"rose-pine": {
label: "Rosé Pine",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 58, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 10, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 33, readerMaxWidth: 900, composerWidth: 940, selectionColor: "#403d52", unreadFontWeight: 720, unreadBadgeStyle: "outline", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
everforest: {
label: "Everforest",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 8, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 960, composerWidth: 940, selectionColor: "#425047", unreadFontWeight: 720, unreadBadgeStyle: "outline", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
synthwave: {
label: "Synthwave",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 6, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 960, composerWidth: 940, selectionColor: "#44306b", unreadFontWeight: 760, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
paper: {
label: "Paper",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "humanist", fontSize: 14, rowHeight: 60, folderPaneWidth: 232, messagePaneWidth: 420, borderRadius: 3, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 760, composerWidth: 940, selectionColor: "#42372a", unreadFontWeight: 700, unreadBadgeStyle: "outline", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
terminal: {
label: "Terminal",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "mono", fontSize: 12, rowHeight: 46, folderPaneWidth: 230, messagePaneWidth: 440, borderRadius: 0, contrast: "normal", iconSize: 16, toolbarDensity: "compact", sidebarItemHeight: 26, readerMaxWidth: 0, composerWidth: 940, selectionColor: "#123d1a", unreadFontWeight: 760, unreadBadgeStyle: "solid", compactHeaders: true, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
aqua: {
label: "Aqua Glass",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 58, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 10, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 33, readerMaxWidth: 960, composerWidth: 940, selectionColor: "#1e3a5f", unreadFontWeight: 720, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
linear: {
label: "Linear inspired",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 46, folderPaneWidth: 232, messagePaneWidth: 440, borderRadius: 6, contrast: "normal", iconSize: 16, toolbarDensity: "compact", sidebarItemHeight: 28, readerMaxWidth: 860, composerWidth: 940, selectionColor: "#1d1f33", unreadFontWeight: 640, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
material: {
label: "Material You inspired",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 14, rowHeight: 60, folderPaneWidth: 256, messagePaneWidth: 440, borderRadius: 12, contrast: "normal", iconSize: 20, toolbarDensity: "normal", sidebarItemHeight: 40, readerMaxWidth: 960, composerWidth: 940, selectionColor: "#4a4458", unreadFontWeight: 700, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
retro: {
label: "Retro 95",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 12, rowHeight: 44, folderPaneWidth: 220, messagePaneWidth: 420, borderRadius: 0, contrast: "normal", iconSize: 16, toolbarDensity: "compact", sidebarItemHeight: 24, readerMaxWidth: 0, composerWidth: 880, selectionColor: "#000080", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: true, showBorders: true, scrollbarDensity: "normal", monochrome: false },
},
};
function resolvedInterfaceSkin(prefs = state.prefs) {
@@ -123,6 +179,23 @@ function activeInterfaceStyle(prefs = state.prefs) {
return interfaceStyleProfiles[resolvedInterfaceSkin(prefs)] || null;
}
const skinDepths = ["colors", "standard", "immersive"];
const presetColorPreferenceKeys = new Set([
"colorPreset", "accentColor", "backgroundColor", "panelColor", "textColor", "selectionColor",
"folderBackground", "messageBackground", "readerBackground",
]);
function resolvedSkinDepth(prefs = state.prefs) {
return skinDepths.includes(prefs.skinDepth) ? prefs.skinDepth : "standard";
}
// "colors" keeps only a predefined style's palette; "standard" and "immersive"
// also apply its fixed layout profile ("immersive" adds the structural tokens
// from 57-skin-tokens.css on top, purely in CSS).
function layoutInterfaceStyle(prefs = state.prefs) {
return resolvedSkinDepth(prefs) === "colors" ? null : activeInterfaceStyle(prefs);
}
const localizedTextNodes = new Map();
const localizedAttributes = new Map();
@@ -191,12 +264,16 @@ function trText(text) {
function applyPreferences() {
const p = state.prefs;
const fixedStyle = activeInterfaceStyle(p);
const paletteStyle = activeInterfaceStyle(p);
const fixedStyle = layoutInterfaceStyle(p);
const skinDepth = resolvedSkinDepth(p);
const visual = fixedStyle
? { ...p, ...fixedStyle.preferences }
: p;
root.dataset.theme = resolveTheme(p.theme);
root.dataset.interfaceSkin = resolvedInterfaceSkin(p);
root.dataset.skinDepth = skinDepth;
root.dataset.skinGradients = String(Boolean(p.skinGradients));
root.dataset.density = visual.density;
root.dataset.preview = visual.preview;
root.dataset.scale = visual.scale;
@@ -235,7 +312,7 @@ function applyPreferences() {
root.style.setProperty("--reader-max-width", Number(visual.readerMaxWidth) > 0 ? `${Number(visual.readerMaxWidth)}px` : "none");
root.style.setProperty("--composer-width", `${Math.max(640, Number(visual.composerWidth || 900))}px`);
root.style.setProperty("--compose-font-family", composeFontCss(p.composeFontFamily));
const lockedRoundcubeSkin = Boolean(fixedStyle);
const lockedRoundcubeSkin = Boolean(paletteStyle);
if (lockedRoundcubeSkin) {
["--selected", "--folder-bg", "--message-bg", "--reader-bg"].forEach((name) => root.style.removeProperty(name));
} else {
@@ -252,7 +329,7 @@ function applyPreferences() {
root.style.setProperty("--radius-control", `${visual.borderRadius}px`);
root.style.setProperty("--radius-panel", `${Math.min(visual.borderRadius + 1, 13)}px`);
const presetCss = fixedStyle ? getComputedStyle(root) : null;
const presetCss = paletteStyle ? getComputedStyle(root) : null;
const presetColor = (name, fallback) => presetCss ? presetCssHex(presetCss, name, fallback) : fallback;
const displayedAccent = presetColor("--accent", p.accentColor);
const displayedBackground = presetColor("--bg", p.backgroundColor);
@@ -262,8 +339,8 @@ function applyPreferences() {
const displayedMessageBackground = presetColor("--message-bg", p.messageBackground || "#181c19");
const displayedReaderBackground = presetColor("--reader-bg", p.readerBackground || "#181c19");
const values = {
"#pref-theme": p.theme, "#pref-interface-skin": p.interfaceSkin, "#pref-density": visual.density, "#pref-preview": visual.preview, "#pref-scale": visual.scale,
"#pref-language": p.language, "#pref-color-preset": fixedStyle ? "custom" : p.colorPreset, "#pref-accent": displayedAccent,
"#pref-theme": p.theme, "#pref-interface-skin": p.interfaceSkin, "#pref-skin-depth": skinDepth, "#pref-density": visual.density, "#pref-preview": visual.preview, "#pref-scale": visual.scale,
"#pref-language": p.language, "#pref-color-preset": paletteStyle ? "custom" : p.colorPreset, "#pref-accent": displayedAccent,
"#pref-background": displayedBackground, "#pref-panel": displayedPanel, "#pref-text-color": displayedText,
"#pref-font-family": visual.fontFamily, "#pref-font-size": visual.fontSize, "#pref-row-height": visual.rowHeight,
"#pref-folder-width": visual.folderPaneWidth, "#pref-message-width": visual.messagePaneWidth, "#pref-radius": visual.borderRadius,
@@ -273,31 +350,42 @@ function applyPreferences() {
"#pref-selection-color": visual.selectionColor, "#pref-unread-weight": visual.unreadFontWeight, "#pref-unread-badge": visual.unreadBadgeStyle, "#pref-scrollbar-density": visual.scrollbarDensity,
};
Object.entries(values).forEach(([selector, value]) => { const node = $(selector); if (node) node.value = value; });
const checks = { "#pref-preset-scale-unlocked": p.presetScaleUnlocked, "#pref-adaptive-email-colors": p.adaptiveEmailColors, "#pref-avatars": p.showAvatars, "#pref-attachment-icons": p.showAttachmentIcons, "#pref-flags": p.showFlags, "#pref-reduce-motion": p.reduceMotion, "#compose-pref-mdn": p.requestMdnDefault, "#compose-pref-attachment-reminder": p.attachmentReminder, "#compose-pref-spellcheck": p.spellcheckEnabled, "#pref-compact-headers": visual.compactHeaders, "#pref-show-borders": visual.showBorders, "#pref-monochrome": visual.monochrome, "#pref-folder-bg-enabled": Boolean(p.folderBackground), "#pref-message-bg-enabled": Boolean(p.messageBackground), "#pref-reader-bg-enabled": Boolean(p.readerBackground) };
const checks = { "#pref-preset-scale-unlocked": p.presetScaleUnlocked, "#pref-skin-gradients": p.skinGradients, "#pref-adaptive-email-colors": p.adaptiveEmailColors, "#pref-avatars": p.showAvatars, "#pref-attachment-icons": p.showAttachmentIcons, "#pref-flags": p.showFlags, "#pref-reduce-motion": p.reduceMotion, "#compose-pref-mdn": p.requestMdnDefault, "#compose-pref-attachment-reminder": p.attachmentReminder, "#compose-pref-spellcheck": p.spellcheckEnabled, "#pref-compact-headers": visual.compactHeaders, "#pref-show-borders": visual.showBorders, "#pref-monochrome": visual.monochrome, "#pref-folder-bg-enabled": Boolean(p.folderBackground), "#pref-message-bg-enabled": Boolean(p.messageBackground), "#pref-reader-bg-enabled": Boolean(p.readerBackground) };
Object.entries(checks).forEach(([selector, value]) => { const node = $(selector); if (node) node.checked = Boolean(value); });
const outputs = { "#pref-accent-value": displayedAccent, "#pref-background-value": displayedBackground, "#pref-panel-value": displayedPanel, "#pref-text-color-value": displayedText, "#pref-font-size-value": `${visual.fontSize} px`, "#pref-row-height-value": `${visual.rowHeight} px`, "#pref-folder-width-value": `${visual.folderPaneWidth} px`, "#pref-message-width-value": `${visual.messagePaneWidth} px`, "#pref-radius-value": `${visual.borderRadius} px`, "#pref-icon-size-value": `${visual.iconSize} px`, "#pref-sidebar-height-value": `${visual.sidebarItemHeight} px`, "#pref-composer-width-value": `${visual.composerWidth} px`, "#pref-selection-color-value": visual.selectionColor, "#pref-unread-weight-value": String(visual.unreadFontWeight) };
Object.entries(outputs).forEach(([selector, value]) => { const node = $(selector); if (node) node.textContent = value; });
[["#pref-folder-bg", displayedFolderBackground], ["#pref-message-bg", displayedMessageBackground], ["#pref-reader-bg", displayedReaderBackground]].forEach(([selector, value]) => { const node = $(selector); if (node) node.value = value; });
const customizationLocked = Boolean(fixedStyle);
[
"#pref-color-preset", "#pref-accent", "#pref-background", "#pref-panel", "#pref-text-color",
const colorControls = [
"#pref-color-preset", "#pref-accent", "#pref-background", "#pref-panel", "#pref-text-color", "#pref-selection-color",
"#pref-folder-bg-enabled", "#pref-folder-bg", "#pref-message-bg-enabled", "#pref-message-bg", "#pref-reader-bg-enabled", "#pref-reader-bg",
];
const layoutControls = [
"#pref-density", "#pref-preview", "#pref-font-family", "#pref-font-size",
"#pref-row-height", "#pref-folder-width", "#pref-message-width", "#pref-radius", "#pref-contrast",
"#pref-icon-size", "#pref-toolbar-density", "#pref-sidebar-height", "#pref-reader-width", "#pref-composer-width",
"#pref-selection-color", "#pref-unread-weight", "#pref-unread-badge", "#pref-scrollbar-density",
"#pref-compact-headers", "#pref-show-borders", "#pref-monochrome",
"#pref-folder-bg-enabled", "#pref-folder-bg", "#pref-message-bg-enabled", "#pref-message-bg", "#pref-reader-bg-enabled", "#pref-reader-bg",
].forEach((selector) => { const node = $(selector); if (node) node.disabled = customizationLocked && !p.presetScaleUnlocked; });
const scaleControl = $("#pref-scale");
if (scaleControl) scaleControl.disabled = customizationLocked && !p.presetScaleUnlocked;
"#pref-unread-weight", "#pref-unread-badge", "#pref-scrollbar-density",
"#pref-compact-headers", "#pref-show-borders", "#pref-monochrome", "#pref-scale",
];
const paletteLocked = Boolean(paletteStyle) && !p.presetScaleUnlocked;
const layoutLocked = Boolean(fixedStyle) && !p.presetScaleUnlocked;
colorControls.forEach((selector) => { const node = $(selector); if (node) node.disabled = paletteLocked; });
layoutControls.forEach((selector) => { const node = $(selector); if (node) node.disabled = layoutLocked; });
const scaleUnlock = $("#pref-preset-scale-unlocked");
if (scaleUnlock) scaleUnlock.disabled = !fixedStyle;
if (scaleUnlock) scaleUnlock.disabled = !paletteStyle;
const scaleUnlockRow = $("#pref-preset-scale-unlocked-row");
if (scaleUnlockRow) scaleUnlockRow.classList.toggle("hidden", !fixedStyle);
if (scaleUnlockRow) scaleUnlockRow.classList.toggle("hidden", !paletteStyle);
const depthNote = $("#pref-skin-depth-note");
if (depthNote) {
depthNote.textContent = ({
colors: tr("skin_depth_colors_note", "Only the style's colors are used. Layout, spacing and typography follow your own settings below."),
standard: tr("skin_depth_standard_note", "Colors plus the style's layout: density, row height, pane widths and type size."),
immersive: tr("skin_depth_immersive_note", "Everything in Standard plus the style's shapes, typography, floating panes, app bar, selection markers and backdrop."),
})[skinDepth];
}
const skinNote = $("#pref-interface-skin-note");
if (skinNote) {
const styleLabel = state.prefs.language === "pl" ? (state.translations._text?.[fixedStyle?.label] || fixedStyle?.label) : fixedStyle?.label;
skinNote.textContent = fixedStyle
const styleLabel = state.prefs.language === "pl" ? (state.translations._text?.[paletteStyle?.label] || paletteStyle?.label) : paletteStyle?.label;
skinNote.textContent = paletteStyle
? `${styleLabel} — ${tr("fixed_interface_style_note", "predefined interface style. Enable preset editing below to create a customizable copy on the first change.")}`
: tr("custom_interface_style_note", "RustBox is fully customizable: colors, spacing, panes, typography and advanced interface controls are available below.");
}
@@ -307,7 +395,7 @@ function normalizeServerPreferences(data) {
if (!data) return;
state.prefs = {
...state.prefs,
theme: data.theme || root.dataset.defaultTheme || "system", interfaceSkin: data.interface_skin || "default", presetScaleUnlocked: Boolean(data.preset_scale_unlocked), density: data.density || "compact", preview: data.preview_pane || "right", composeFormat: data.compose_format || "html",
theme: data.theme || root.dataset.defaultTheme || "system", interfaceSkin: data.interface_skin || "default", skinDepth: skinDepths.includes(data.skin_depth) ? data.skin_depth : "standard", skinGradients: Boolean(data.skin_gradients), presetScaleUnlocked: Boolean(data.preset_scale_unlocked), density: data.density || "compact", preview: data.preview_pane || "right", composeFormat: data.compose_format || "html",
composeAutosaveMs: Number(data.compose_autosave_ms || 3000), defaultPriority: data.default_priority || "normal", requestMdnDefault: Boolean(data.request_mdn_default), attachmentReminder: data.attachment_reminder !== false, composeFontFamily: data.compose_font_family || "system", replyPosition: data.reply_position || "above", quoteStyle: data.quote_style || "standard", forwardMode: data.forward_mode || "inline", spellcheckEnabled: data.spellcheck_enabled !== false,
scale: data.ui_scale || "normal", language: data.language || "en", colorPreset: data.color_preset || "sage",
accentColor: data.accent_color || "#6f9c86", backgroundColor: data.background_color || "#121513", panelColor: data.panel_color || "#181c19", textColor: data.text_color || "#edf0ec", fontFamily: data.font_family || "system", fontSize: data.font_size || 13,
@@ -477,7 +565,7 @@ function presetCssHex(styles, name, fallback) {
function customAppearancePayload(prefs) {
const keys = [
"interfaceSkin", "presetScaleUnlocked", "colorPreset", "accentColor", "backgroundColor", "panelColor", "textColor",
"interfaceSkin", "skinDepth", "skinGradients", "presetScaleUnlocked", "colorPreset", "accentColor", "backgroundColor", "panelColor", "textColor",
"density", "preview", "scale", "fontFamily", "fontSize", "rowHeight", "folderPaneWidth", "messagePaneWidth",
"borderRadius", "contrast", "iconSize", "toolbarDensity", "sidebarItemHeight", "readerMaxWidth", "composerWidth",
"selectionColor", "unreadFontWeight", "unreadBadgeStyle", "scrollbarDensity", "compactHeaders", "showBorders", "monochrome",
@@ -511,13 +599,19 @@ function schedulePromotedAppearanceSave() {
}
function promotePresetToCustom(key, value) {
const fixedStyle = activeInterfaceStyle(state.prefs);
if (!fixedStyle || !state.prefs.presetScaleUnlocked || !presetEditablePreferenceKeys.has(key)) return false;
const paletteStyle = activeInterfaceStyle(state.prefs);
if (!paletteStyle || !presetEditablePreferenceKeys.has(key)) return false;
// At the "colors" depth the layout already follows the user's own settings,
// so only a palette change needs a custom copy of the style.
const layoutStyle = layoutInterfaceStyle(state.prefs);
if (!layoutStyle && !presetColorPreferenceKeys.has(key)) return false;
if (!state.prefs.presetScaleUnlocked) return false;
const fixedStyle = paletteStyle;
const styles = getComputedStyle(root);
state.prefs = {
...state.prefs,
...fixedStyle.preferences,
...(layoutStyle ? layoutStyle.preferences : {}),
interfaceSkin: "rustbox",
presetScaleUnlocked: false,
colorPreset: "custom",
@@ -566,7 +660,7 @@ function savePreference(key, value) {
}
async function resetAppearance() {
const payload = { theme: root.dataset.defaultTheme || "system", interface_skin: "default", preset_scale_unlocked: false, density: "compact", preview_pane: "right", ui_scale: "normal", color_preset: "sage", accent_color: "#6f9c86", background_color: "#121513", panel_color: "#181c19", text_color: "#edf0ec", font_family: "system", font_size: 13, row_height: 58, folder_pane_width: 228, message_pane_width: 420, border_radius: 4, contrast: "normal", show_avatars: true, show_attachment_icons: true, show_flags: true, reduce_motion: false, date_format: "locale", time_format: "locale", icon_size: 18, toolbar_density: "normal", sidebar_item_height: 29, reader_max_width: 0, composer_width: 900, selection_color: "#28352e", unread_font_weight: 730, unread_badge_style: "dot", compact_headers: false, show_borders: true, scrollbar_density: "thin", monochrome: false, folder_background: "", message_background: "", reader_background: "", adaptive_email_colors: false };
const payload = { theme: root.dataset.defaultTheme || "system", interface_skin: "default", skin_depth: "standard", skin_gradients: false, preset_scale_unlocked: false, density: "compact", preview_pane: "right", ui_scale: "normal", color_preset: "sage", accent_color: "#6f9c86", background_color: "#121513", panel_color: "#181c19", text_color: "#edf0ec", font_family: "system", font_size: 13, row_height: 58, folder_pane_width: 228, message_pane_width: 420, border_radius: 4, contrast: "normal", show_avatars: true, show_attachment_icons: true, show_flags: true, reduce_motion: false, date_format: "locale", time_format: "locale", icon_size: 18, toolbar_density: "normal", sidebar_item_height: 29, reader_max_width: 0, composer_width: 900, selection_color: "#28352e", unread_font_weight: 730, unread_badge_style: "dot", compact_headers: false, show_borders: true, scrollbar_density: "thin", monochrome: false, folder_background: "", message_background: "", reader_background: "", adaptive_email_colors: false };
try {
normalizeServerPreferences(await request("/settings/display", { method: "PUT", body: JSON.stringify(payload) }));
state.mailPrefs = await request("/settings/mailbox", { method: "PUT", body: JSON.stringify({ compact_wide_rows: true }) });
+2
View File
@@ -423,6 +423,8 @@ 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(); if (state.authPolicy.pgpEnabled !== false) loadOpenPgp(); } if (button.dataset.tab === "openpgp" && state.authPolicy.pgpEnabled !== false) 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); if (state.currentDetail && state.prefs.adaptiveEmailColors) renderMessageDetail(state.currentDetail); });
$("#pref-interface-skin").addEventListener("change", (event) => savePreference("interfaceSkin", event.target.value));
$("#pref-skin-depth").addEventListener("change", (event) => savePreference("skinDepth", event.target.value));
$("#pref-skin-gradients").addEventListener("change", (event) => savePreference("skinGradients", event.target.checked));
$("#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));