From fa724008596e54531b4fd49b240d6c9061a54e86 Mon Sep 17 00:00:00 2001 From: Tero Date: Mon, 31 Aug 2026 21:32:58 +0300 Subject: [PATCH] add docker support --- LICENSE | 5 + README.md | 124 +++++++++++++ eternity/meta.json | 8 + eternity/styles/styles.css | 345 +++++++++++++++++++++++++++++++++++++ 4 files changed, 482 insertions(+) create mode 100644 LICENSE create mode 100644 eternity/meta.json create mode 100644 eternity/styles/styles.css diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e7e5ddb --- /dev/null +++ b/LICENSE @@ -0,0 +1,5 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +This skin is distributed under the GNU Affero General Public License, version 3 or later. +The complete license text is available at https://www.gnu.org/licenses/agpl-3.0.txt. diff --git a/README.md b/README.md index e69de29..4fa9f4f 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,124 @@ +# Eternity Roundcube Skin + +`Eternity` is a sharp editorial skin for Roundcube, built in the visual language of Eternity Project: paper-white surfaces, ink-black structure, acid-green actions, and orange signals. It extends Roundcube's maintained Elastic skin, retaining Elastic's responsive layout and interaction model while changing the visual treatment. + +## Requirements + +- Roundcube 1.6.x +- The bundled `elastic` skin must remain installed +- A web server able to serve static CSS files from Roundcube's `skins` directory + +The skin has no build step and no PHP dependencies. Its font stacks use `Space Grotesk` and `DM Mono` when those fonts are available, then fall back to locally installed system fonts. This keeps the login screen usable without a third-party font request. + +## Install + +1. Copy the `eternity` directory into the Roundcube installation's `skins` directory: + + ```sh + cp -R eternity /var/www/roundcube/skins/ + ``` + +2. Ensure the web-server user can read the copied files: + + ```sh + chown -R www-data:www-data /var/www/roundcube/skins/eternity + find /var/www/roundcube/skins/eternity -type d -exec chmod 755 {} \; + find /var/www/roundcube/skins/eternity -type f -exec chmod 644 {} \; + ``` + +3. Set the default skin in Roundcube's configuration. Add or update this line in `config/config.inc.php`: + + ```php + $config['skin'] = 'eternity'; + ``` + +4. Clear the browser cache, then load Roundcube. The login view, mailbox list, compose view, menus, controls, and status messages should all use the new palette. + +## Docker Install + +### Docker Compose (persistent) + +Place this repository alongside the Compose file, then bind-mount the skin into the Roundcube service. The official `roundcube/roundcubemail` image exposes its application at `/var/www/html`. + +```yaml +services: + roundcube: + volumes: + - ./ep-roundcube-skin/eternity:/var/www/html/skins/eternity:ro + - ./roundcube-config/config.inc.php:/var/roundcube/config/config.inc.php:ro +``` + +Create `./roundcube-config/config.inc.php` if it does not already exist and include the skin setting with the rest of the deployment configuration: + +```php +/dev/null'` to identify the active configuration file. Restart the service after changing either mount: + +```sh +docker compose up -d --force-recreate roundcube +``` + +### Existing Running Container + +Use this method to test the skin on a container that is already running. Replace `roundcube` with the container name or ID shown by `docker ps`. + +1. Copy the skin from this repository into the container: + + ```sh + docker cp ./eternity roundcube:/var/www/html/skins/eternity + ``` + +2. Confirm Roundcube can see the skin files: + + ```sh + docker exec roundcube sh -c 'test -f /var/www/html/skins/eternity/meta.json && test -f /var/www/html/skins/eternity/styles/styles.css' + ``` + +3. Add `$config['skin'] = 'eternity';` to the active `config.inc.php` in the container, then restart it: + + ```sh + docker restart roundcube + ``` + +This live-container approach is not persistent: `docker compose up --force-recreate`, `docker rm`, or an image update removes the copied skin and in-container configuration change. Once the skin is confirmed, add the two bind mounts from the Compose example so both survive container recreation. + +After either deployment method, hard-refresh the browser or clear its cache. If the old appearance remains, inspect the browser network panel to confirm that `/skins/eternity/styles/styles.css` returns HTTP 200. + +## Structure + +```text +eternity/ + meta.json Skin registration and Elastic inheritance + styles/styles.css Visual overrides for Elastic +``` + +`styles/styles.css` imports Elastic before applying Eternity's overrides. Do not remove the import: the base skin provides the complete component layout and image assets. + +## Customize + +The visual palette is declared at the top of `eternity/styles/styles.css`: + +| Token | Value | Role | +| --- | --- | --- | +| `--ink` | `#11212b` | Primary text, borders, and primary controls | +| `--paper` | `#f1f1e9` | Page and surface background | +| `--acid` | `#c7ef4b` | Primary hover, selection, and success state | +| `--orange` | `#ff6b35` | Links, secondary hover, quota, and error signal | +| `--line` | `#b9c5bc` | Quiet dividers | +| `--muted` | `#59706b` | Secondary text | + +Change these variables to retheme the interface while preserving the semantic states. Test login, inbox, compose, contacts, settings, search, and mobile navigation after any update to Roundcube or Elastic. + +## Upgrade Notes + +This is an Elastic child skin. When updating Roundcube, keep the `eternity` directory in place and confirm that the upstream Elastic stylesheet remains at `skins/elastic/styles/styles.css`. Recopy the skin after deployments that replace the whole Roundcube `skins` directory. + +The skin intentionally uses direct CSS selectors for stable Roundcube surfaces and CSS custom properties for broad Elastic integration. If a future Roundcube release renames markup, inspect that release's Elastic stylesheet and update the matching override selector here. + +## License + +Licensed under the GNU Affero General Public License, version 3 or later. See [LICENSE](LICENSE). diff --git a/eternity/meta.json b/eternity/meta.json new file mode 100644 index 0000000..9824679 --- /dev/null +++ b/eternity/meta.json @@ -0,0 +1,8 @@ +{ + "name": "Eternity", + "author": "Eternity Project", + "license": "GNU Affero General Public License", + "version": "1.0.0", + "extends": "elastic", + "localization": false +} diff --git a/eternity/styles/styles.css b/eternity/styles/styles.css new file mode 100644 index 0000000..312e268 --- /dev/null +++ b/eternity/styles/styles.css @@ -0,0 +1,345 @@ +@import url("../../elastic/styles/styles.css"); + +:root { + --ink: #11212b; + --paper: #f1f1e9; + --acid: #c7ef4b; + --orange: #ff6b35; + --line: #b9c5bc; + --muted: #59706b; + --body-font-family: "Space Grotesk", "Helvetica Neue", sans-serif; + --header-font-family: "Space Grotesk", "Helvetica Neue", sans-serif; + --font-family-monospace: "DM Mono", "Courier New", monospace; + --primary: var(--ink); + --primary-text: var(--paper); + --secondary: var(--acid); + --secondary-text: var(--ink); + --body-bg: var(--paper); + --content-bg: var(--paper); + --layout-bg: var(--paper); + --main-action-color: var(--ink); + --main-action-text-color: var(--paper); + --link-color: var(--ink); + --input-focus-border-color: var(--ink); + --input-focus-box-shadow: 0 0 0 3px var(--acid); + --border-radius: 0; +} + +html, +body, +#layout, +#layout-content, +#layout-list, +#layout-sidebar, +#layout-menu { + background: var(--paper); + color: var(--ink); +} + +body { + font-family: var(--body-font-family); + background-image: linear-gradient(90deg, transparent 49.5%, rgb(17 33 43 / 5%) 50%, transparent 50.5%), linear-gradient(rgb(17 33 43 / 4%) 1px, transparent 1px); + background-size: 70px 70px, 7px 7px; +} + +body > #layout, +#layout { + position: relative; + z-index: 0; +} + +#layout::before { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + background: rgb(241 241 233 / 82%); +} + +body a, +body .link, +#taskmenu a, +#taskmenu a:visited { + color: var(--ink); +} + +body a:hover, +body .link:hover, +#taskmenu a:hover, +#taskmenu a.selected { + color: var(--orange); +} + +#logo, +#layout > .header, +#layout-sidebar .header, +#layout-list .header, +#layout-content .header { + border-color: var(--ink); +} + +#logo { + font-family: var(--font-family-monospace); + font-weight: 500; + letter-spacing: 1px; +} + +#taskmenu, +#taskmenu .action-buttons, +#layout-menu .menu, +.toolbar, +#messagelist-header, +#directorylist-header { + font-family: var(--font-family-monospace); + font-size: 12px; + letter-spacing: 0.3px; +} + +#taskmenu .action-buttons a, +#taskmenu a, +#layout-menu .menu a { + border-radius: 0; +} + +#taskmenu .action-buttons a.selected, +#taskmenu .action-buttons a:hover, +#taskmenu a.selected, +#layout-menu .menu a.selected, +#layout-menu .menu a:hover { + background: var(--acid); + color: var(--ink); +} + +.button, +.btn, +input[type="button"], +input[type="submit"], +button, +.formbuttons .button { + border: 1px solid var(--ink); + border-radius: 0; + box-shadow: none; + background: var(--ink); + color: var(--paper); + font-family: var(--font-family-monospace); + font-size: 12px; + letter-spacing: 0.3px; +} + +.button:hover, +.btn:hover, +input[type="button"]:hover, +input[type="submit"]:hover, +button:hover, +.formbuttons .button:hover { + border-color: var(--ink); + background: var(--acid); + color: var(--ink); + box-shadow: none; +} + +.button.secondary, +.btn-secondary, +.button.btn-secondary { + background: transparent; + color: var(--ink); +} + +.button.secondary:hover, +.btn-secondary:hover, +.button.btn-secondary:hover { + background: var(--orange); +} + +input[type="text"], +input[type="password"], +input[type="email"], +input[type="search"], +textarea, +select, +.custom-select, +.form-control { + border: 1px solid var(--ink); + border-radius: 0; + box-shadow: none; + background-color: #fbfbf6; + color: var(--ink); + font-family: var(--body-font-family); +} + +input:focus, +textarea:focus, +select:focus, +.custom-select:focus, +.form-control:focus { + border-color: var(--ink); + outline: 3px solid var(--acid); + outline-offset: 1px; + box-shadow: none; +} + +.form-group > label, +.form-label, +.propform label, +#login-form label { + color: var(--ink); + font-family: var(--font-family-monospace); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.7px; + text-transform: uppercase; +} + +#login-form, +#login-footer, +#login-content, +#login-box { + background: transparent; +} + +#login-form .form-group, +#login-box { + border-color: var(--ink); + border-radius: 0; +} + +.listing, +.records-table, +.table, +#messagelist, +#directorylist, +#contacts-table { + border-color: var(--ink); + background: var(--paper); +} + +.records-table thead td, +.records-table thead th, +#messagelist thead td, +#directorylist .treelist li a { + border-color: var(--ink); + color: var(--muted); + font-family: var(--font-family-monospace); + font-size: 11px; + letter-spacing: 0.5px; +} + +.records-table tbody tr td, +#messagelist tbody tr td, +#directorylist .treelist li a { + border-color: var(--line); +} + +.records-table tbody tr:hover td, +#messagelist tbody tr:hover td, +#directorylist .treelist li a:hover { + background: var(--acid); + color: var(--ink); +} + +.records-table tbody tr.selected td, +#messagelist tbody tr.selected td, +#directorylist .treelist li.selected > a { + background: var(--ink); + color: var(--paper); +} + +.records-table tbody tr.selected .unread, +#messagelist tbody tr.selected .unread { + color: var(--paper); +} + +.box, +.card, +#compose-headers, +#compose-content, +#message-content, +#mailview-left, +#mailview-right { + border-color: var(--ink); + border-radius: 0; + box-shadow: none; + background: var(--paper); +} + +#message-header, +.message-header, +#compose-headers { + border-color: var(--ink); + background: var(--paper); +} + +#compose-headers .form-control, +#compose-headers input, +#compose-headers textarea { + background: #fbfbf6; +} + +#layout-sidebar .pagenav, +#layout-list .pagenav, +#layout-sidebar .footer, +#layout-list .footer { + border-color: var(--ink); + background: var(--paper); +} + +.popover, +.menu, +.ui-autocomplete, +.dropdown-menu, +#rcmKSearchpane { + border: 1px solid var(--ink); + border-radius: 0; + box-shadow: 4px 4px 0 rgb(17 33 43 / 16%); + background: var(--paper); +} + +.popover .menu li a:hover, +.menu li a:hover, +.ui-autocomplete .ui-menu-item-wrapper.ui-state-active, +.dropdown-item:hover { + background: var(--acid); + color: var(--ink); +} + +.alert, +#messagestack > div, +#messagestack > .alert { + border: 1px solid var(--ink); + border-radius: 0; + box-shadow: none; + font-family: var(--font-family-monospace); +} + +.alert-success, +#messagestack .success { + background: var(--acid); + color: var(--ink); +} + +.alert-danger, +.alert-error, +#messagestack .error { + border-color: var(--orange); + background: #ffe3d9; + color: var(--ink); +} + +.quota-widget .value, +.progress-bar { + background: var(--orange); +} + +@media (max-width: 768px) { + #taskmenu .action-buttons a, + #taskmenu a, + #layout-menu .menu a { + min-width: 2.75rem; + } + + .records-table tbody tr td, + #messagelist tbody tr td { + font-size: 0.9rem; + } +}