add docker support

This commit is contained in:
2026-08-31 21:32:58 +03:00
parent 94b8f7ec05
commit fa72400859
4 changed files with 482 additions and 0 deletions
+5
View File
@@ -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.
+124
View File
@@ -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
<?php
$config['skin'] = 'eternity';
```
The configuration mount location can vary for non-official images. Check the image documentation or run `docker compose exec roundcube sh -c 'find / -path "*/config.inc.php" 2>/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).
+8
View File
@@ -0,0 +1,8 @@
{
"name": "Eternity",
"author": "Eternity Project",
"license": "GNU Affero General Public License",
"version": "1.0.0",
"extends": "elastic",
"localization": false
}
+345
View File
@@ -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;
}
}