add docker support
This commit is contained in:
@@ -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.
|
||||
@@ -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).
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Eternity",
|
||||
"author": "Eternity Project",
|
||||
"license": "GNU Affero General Public License",
|
||||
"version": "1.0.0",
|
||||
"extends": "elastic",
|
||||
"localization": false
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user