Soft dark themes for FreeScout (Slate, Graphite, Nord, Mocha, Auto)
  • Python 58.9%
  • PHP 24.7%
  • Blade 5.8%
  • Shell 5.5%
  • CSS 5.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
JD b3d2d17965
All checks were successful
ci / battery (push) Successful in 27s
CI: manual "Run workflow" button
2026-10-15 01:12:53 +08:00
.forgejo/workflows CI: manual "Run workflow" button 2026-10-15 01:12:53 +08:00
doc Release process: annotated tags, which --follow-tags pushes 2026-10-15 00:02:19 +08:00
img FreeScout modules 1.0.0: JdneerThemes 2026-10-05 16:19:48 +08:00
module release: 1.0.1 2026-10-15 00:03:10 +08:00
scripts Standalone repository: own check, zip and version scripts, CI and release workflows, release process 2026-10-14 20:22:45 +08:00
tests/in-app FreeScout modules 1.0.0: JdneerThemes 2026-10-05 16:19:48 +08:00
theme FreeScout modules 1.0.0: JdneerThemes 2026-10-05 16:19:48 +08:00
tools FreeScout modules 1.0.0: JdneerThemes 2026-10-05 16:19:48 +08:00
.gitignore Standalone repository: own check, zip and version scripts, CI and release workflows, release process 2026-10-14 20:22:45 +08:00
build.sh FreeScout modules 1.0.0: JdneerThemes 2026-10-05 16:19:48 +08:00
CHANGELOG.md release: 1.0.1 2026-10-15 00:03:10 +08:00
LICENSE Standalone repository: own check, zip and version scripts, CI and release workflows, release process 2026-10-14 20:22:45 +08:00
NOTICE Standalone repository: own check, zip and version scripts, CI and release workflows, release process 2026-10-14 20:22:45 +08:00
README.md Standalone repository: own check, zip and version scripts, CI and release workflows, release process 2026-10-14 20:22:45 +08:00

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

  1. Download JdneerThemes-<version>.zip and SHA256SUMS from the repository's Releases page, and check the download: sha256sum -c --ignore-missing SHA256SUMS.

  2. Unpack it into the Modules directory of your FreeScout installation (the directory that contains artisan). The zip holds a single JdneerThemes/ folder:

    cd /path/to/freescout
    unzip /path/to/JdneerThemes-<version>.zip -d Modules/
    
  3. Give the folder to the user your web server runs as (often www-data, nginx or apache):

    chown -R www-data:www-data Modules/JdneerThemes
    
  4. In FreeScout, open Manage → Modules and click Activate on JDNEER Themes. Activation creates the module's table and its public folder link.

  5. 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 Light: conversation list Light: conversation
Slate Slate: conversation list Slate: conversation
Graphite Graphite: conversation list Graphite: conversation
Nord Nord: conversation list Nord: conversation
Mocha Mocha: conversation list Mocha: conversation

On a phone (Slate):

Slate: conversation list on a phone

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

  1. Set version in theme/upstream.toml, run python3 -m jdneerthemes fetch --theme ../theme --update-pins from tools/, and paste the printed pins after checking the files changed where the release notes say they did.
  2. ./build.sh: fails and lists any new colours. Map them (--suggest, then review) and rebuild.
  3. FreeScout's marketplace modules are not public, so their CSS is not fetched. After a module update, copy its Modules/<Name>/Public/css/*.css from an installation into a folder and run python3 -m jdneerthemes build --snapshot <folder> --theme ../theme --out /tmp/x --suggest from tools/ to see its colours, then theme them by hand in theme/modules.css.

Platform notes (FreeScout 1.8.244, checked 2026-10-04)

  • Composer PSR-4 maps Modules\\ → Modules/, so module classes autoload and start.php needs no require_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>/Public is created on activation. \Module::getPublicPath($alias) returns /modules/<alias>.
  • App\User::ROLE_USER = 1, ROLE_ADMIN = 2. \Helper::getSubdirectory() exists. Flash messages use the key flash_success_floating.
  • Stylesheets are bundled by devfactory/minify with the old CssMinifier. Tested: it keeps custom properties, :has(), color-mix(), nested @media, !important and ::selection.
  • module:migrate wants 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.