- Python 58.9%
- PHP 24.7%
- Blade 5.8%
- Shell 5.5%
- CSS 5.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| doc | ||
| img | ||
| module | ||
| scripts | ||
| tests/in-app | ||
| theme | ||
| tools | ||
| .gitignore | ||
| build.sh | ||
| CHANGELOG.md | ||
| LICENSE | ||
| NOTICE | ||
| README.md | ||
JDNEER Themes
Per-user soft dark themes for FreeScout. Light stays the default and is untouched: users on Light get neither a body class nor the theme stylesheet.
Requirements
FreeScout 1.8.0 or later (tested with 1.8.244), and a browser with CSS color-mix() support (any current one).
Install
-
Download
JdneerThemes-<version>.zipandSHA256SUMSfrom the repository's Releases page, and check the download:sha256sum -c --ignore-missing SHA256SUMS. -
Unpack it into the
Modulesdirectory of your FreeScout installation (the directory that containsartisan). The zip holds a singleJdneerThemes/folder:cd /path/to/freescout unzip /path/to/JdneerThemes-<version>.zip -d Modules/ -
Give the folder to the user your web server runs as (often
www-data,nginxorapache):chown -R www-data:www-data Modules/JdneerThemes -
In FreeScout, open Manage → Modules and click Activate on JDNEER Themes. Activation creates the module's table and its public folder link.
-
Optional: pick a default for everyone under Settings → Appearance. Each user can override it under Profile → Appearance.
Nothing changes for anyone until a theme is chosen: Light stays FreeScout's own look.
Update
Unpack the new zip next to the old folder and swap them, so the module is never half-present while FreeScout is serving requests, then let FreeScout pick up the new version:
cd /path/to/freescout
rm -rf /tmp/jdneerthemes-new /tmp/jdneerthemes-old
unzip /path/to/JdneerThemes-<new version>.zip -d /tmp/jdneerthemes-new
mv Modules/JdneerThemes /tmp/jdneerthemes-old && mv /tmp/jdneerthemes-new/JdneerThemes Modules/
chown -R www-data:www-data Modules/JdneerThemes
sudo -u www-data php artisan freescout:module-install jdneerthemes # migrations, public link, cache
Users keep their settings. If something looks wrong, swap the old folder back and run the last command again.
Uninstall
Manage → Modules → JDNEER Themes → Deactivate puts everyone back on Light immediately. Delete on the same
card then removes the module's files. Users' choices stay in the jdneer_user_themes table and the global
default in the options table (jdneerthemes_*), so re-installing brings them back; drop them by hand to remove
every trace.
For users
Profile → Appearance:
- Theme: Light, Auto (follow system), Slate, Graphite, Nord, Mocha. Auto uses a chosen dark theme when the operating system is in dark mode.
- Use the theme's accent colour instead of the brand colour (off by default). By default, dark themes take the brand colour from Settings → Customization → Header Color: a tinted navbar with a brand line, plus brand buttons and focus rings.
- Show emails in theme colours (off by default). Message bodies are shown in theme colours; this is display only. By default, message bodies sit on a dimmed light card.
Admins can open any user's Appearance page; users can only open their own.
Screenshots
Mockups rendered from FreeScout 1.8.244's own stylesheets and the generated theme, with invented data
(regenerate with python3 tools/mockups.py). Dark themes show the default brand accent.
| Theme | Conversation list | Conversation |
|---|---|---|
| Light | ![]() |
![]() |
| Slate | ![]() |
![]() |
| Graphite | ![]() |
![]() |
| Nord | ![]() |
![]() |
| Mocha | ![]() |
![]() |
On a phone (Slate):
Development
| Path | What |
|---|---|
module/ |
The module itself: exactly what the release zip contains (plus LICENSE, NOTICE, CHANGELOG) |
module/Public/css/themes.css, previews.css |
Generated by build.sh, not committed |
theme/ |
Sources: upstream.toml (pinned FreeScout version + SHA-256 of its stylesheets), themes.toml (roles, contrast gates), roles.toml (colour → role map + pins), palettes/, modules.css (hand-written rules for FreeScout modules), extra.css, previews.css |
tools/ |
Generator (python3 -m jdneerthemes, stdlib only), its tests, mockups.py (README screenshots) |
img/ |
README screenshots generated by tools/mockups.py |
.cache/ |
Downloaded FreeScout stylesheets, not committed |
tests/in-app/check_themes.php |
Checks run inside a FreeScout installation |
./build.sh # fetch pinned FreeScout CSS, unit tests, generate module/Public/css (fails on unmapped colours or contrast)
./build.sh --suggest # draft roles for unmapped colours (paste reviewed lines into theme/roles.toml)
python3 tools/mockups.py # after visual changes: regenerate img/ (needs google-chrome or chromium)
scripts/build-zip.sh # the release zip and checksums in dist/, as in Install
To try a build on a test FreeScout, copy module/ to Modules/JdneerThemes there (after ./build.sh) and
follow Install or Update. FreeScout names its combined CSS after the module version, so give every test build a
new version: scripts/set-version.sh 1.0.1-dev1. Record user-visible changes in CHANGELOG.md under
[Unreleased]. Releases follow doc/RELEASE-PROCESS.md.
In-app checks, on a test installation with the module active (they use a synthetic user and fail if any real user's row or global option changes):
cd /path/to/freescout
sudo -u www-data php /path/to/JdneerThemes/tests/in-app/check_themes.php --expect-table
After a FreeScout upgrade
- Set
versionintheme/upstream.toml, runpython3 -m jdneerthemes fetch --theme ../theme --update-pinsfromtools/, and paste the printed pins after checking the files changed where the release notes say they did. ./build.sh: fails and lists any new colours. Map them (--suggest, then review) and rebuild.- FreeScout's marketplace modules are not public, so their CSS is not fetched. After a module update, copy its
Modules/<Name>/Public/css/*.cssfrom an installation into a folder and runpython3 -m jdneerthemes build --snapshot <folder> --theme ../theme --out /tmp/x --suggestfromtools/to see its colours, then theme them by hand intheme/modules.css.
Platform notes (FreeScout 1.8.244, checked 2026-10-04)
- Composer PSR-4 maps
Modules\\→Modules/, so module classes autoload andstart.phpneeds norequire_once. layouts/app.blade.php:@action('layout.head')sits inside<head>(brand style),@action('body.class')inside the<body class="…">attribute, and@yield('javascript')inside a CSP-nonce script.public/modules/<alias>→Modules/<Name>/Publicis created on activation.\Module::getPublicPath($alias)returns/modules/<alias>.App\User::ROLE_USER = 1,ROLE_ADMIN = 2.\Helper::getSubdirectory()exists. Flash messages use the keyflash_success_floating.- Stylesheets are bundled by devfactory/minify with the old
CssMinifier. Tested: it keeps custom properties,:has(),color-mix(), nested@media,!importantand::selection. module:migratewants the display name ("JDNEER Themes").freescout:module-install <alias>is the reliable path.- FreeScout auto-deactivates a module whose listed files fail to load. Never delete a file a live module lists before its cache is cleared; that is why Update swaps folders instead of deleting first.









