Inspired by the "Modern responsive Navbar with Dark/Light Theme" shared by ppradhan@live.com, I developed a solution for PHPRunner 11.1. Since the original method did not work for me, I created a listbox (dropdown) next to the username to select themes, paired with a toggle button for switching between Day and Night modes.The Challenge with PHPRunner and LESSDuring the build process, PHPRunner converts Bootstrap 3 LESS files into standard CSS, making direct theme manipulation difficult. I noticed that the compiled style.css files are stored in C:\runnerapps\your-project\styles\bootstrap. By leveraging this, I found a way to implement a stable theme switcher.Step 1: Modifying the Theme Variables
- I used the Bootstrap Live Customizer to obtain the Amelia theme and download its variables.less file.
- I modified the variables.less file to include a @night-mode toggle and logic for dynamic color shifts.
Example Logic for variables.less:
@night-mode: false;
// Gray and brand colors
@gray-base: if(@night-mode, #ffffff, #000000);
@gray-anti-base: if(@night-mode, #000000, #ffffff);
@gray-darker: mix(@gray-anti-base, @gray-base, 13.5%);
@gray-dark: mix(@gray-anti-base, @gray-base, 20%);
@gray: mix(@gray-anti-base, @gray-base, 33.5%);
@gray-light: mix(@gray-anti-base, @gray-base, 60%);
@gray-lighter: mix(@gray-anti-base, @gray-base, 93.5%);
// Base Brand Colors
@brand-primary-base: #ad1d28;
@brand-success-base: #48ca3b;
@brand-info-base: #4d3a7d;
@brand-warning-base: #debb27;
@brand-danger-base: #df6e1e;
// Dynamic Theme Colors
@brand-primary: if(@night-mode, tint(@brand-primary-base, 30%), @brand-primary-base);
@brand-success: if(@night-mode, tint(@brand-success-base, 30%), @brand-success-base);
@brand-info: if(@night-mode, tint(@brand-info-base, 30%), @brand-info-base);
@brand-warning: if(@night-mode, tint(@brand-warning-base, 30%), @brand-warning-base);
@brand-danger: if(@night-mode, tint(@brand-danger-base, 30%), @brand-danger-base);
// Scaffolding
@body-bg-base: #108a93;
@text-color-base: @gray-lighter;
@body-bg: if(@night-mode, mix(#000000, @body-bg-base, 90%), @body-bg-base);
@text-color: if(@night-mode, @gray-darker, @text-color-base);
@table-border-color: if(@night-mode, lighten(@body-bg, 8%), darken(@body-bg, 5%));
... rest of amelia variables.less
Step 2: Directory Setup
- Using Gemini AI, I created generic bootswatch.less and tweaks.less files.
- I placed these, along with the modified variables.less, into a new folder named "Amelia" within the PHPRunner installation path:
C:\Users\user\AppData\Local\PHPRunner111\app-11.1.44023\resources\styles\less\bootswatch\Amelia. - To create the Dark version, I copied the "Amelia" folder to a new directory called "Amelia-night" and simply changed the first line of variables.less to @night-mode: true;.
Step 3: Deployment and JavaScriptAfter building the project with both themes selected, PHPRunner generates the corresponding CSS in C:\runnerapps\your-project\styles\bootstrap\amelia and ...\amelia-night.
I then added the following logic to custom-functions.js to handle the UI elements and local storage:
Initialization: The ThemeManager.init() function checks localStorage for a saved theme and applies it.
UI Injection: It uses a MutationObserver and setInterval to ensure the dropdown and toggle button are rendered next to the login/username button.
Theme Switching: When a user selects a new theme or toggles the mode, the script updates the <link> tag in the <head> of the document.
Error Handling: If a "night" version of a theme does not exist, the script automatically falls back to the "normal" version.
Important Deployment Note
When uploading your application to a host, ensure you manually transfer all generated theme folders within styles\bootstrap to the root directory of your server to ensure the switcher can find the necessary CSS files.Would you like me to refine the JavaScript code further to optimize the performance of the MutationObserver?
The code in custom-functions.js is
/ ===========================================
THEME SWITCHER (Bootstrap 3 / PHPRunner )
=========================================== /
// ThemeManager.init();
var ThemeManager = {
selectId: 'theme_selector_dropdown',
toggleId: 'theme_night_toggle',
// Η λίστα σου με τα "Βασικά" θέματα
availableThemes: [
{ name: 'amelia', path: 'styles/bootstrap/amelia/normal/style.css' },
{ name: 'Cerulean', path: 'styles/bootstrap/cerulean/normal/style.css' },
{ name: 'Cosmo', path: 'styles/bootstrap/cosmo/normal/style.css' },
{ name: 'Cyborg', path: 'styles/bootstrap/cyborg/normal/style.css' },
{ name: 'darkly', path: 'styles/bootstrap/darkly/normal/style.css' },
{ name: 'default', path: 'styles/bootstrap/default/normal/style.css' },
{ name: 'flatly', path: 'styles/bootstrap/flatly/normal/style.css' },
{ name: 'journal', path: 'styles/bootstrap/journal/normal/style.css' },
{ name: 'litera', path: 'styles/bootstrap/litera/normal/style.css' },
{ name: 'Materia', path: 'styles/bootstrap/materia/normal/style.css' },
{ name: 'morph', path: 'styles/bootstrap/morph/normal/style.css' },
{ name: 'simplex', path: 'styles/bootstrap/simplex/normal/style.css' },
{ name: 'slate', path: 'styles/bootstrap/slate/normal/style.css' },
{ name: 'superhero', path: 'styles/bootstrap/superhero/normal/style.css' },
{ name: 'simplex', path: 'styles/bootstrap/simplex/normal/style.css' }
],
init: function () {
var self = this;
var savedTheme = localStorage.getItem('user-selected-theme-path');
if (savedTheme) this.applyTheme(savedTheme);
var observer = new MutationObserver(function() { self.render(); });
observer.observe(document.body, { childList: true, subtree: true });
setInterval(function() { self.render(); }, 1000);
this.render();
},
render: function () {
if (document.getElementById(this.selectId)) return;
var loginSpan = document.querySelector('[data-itemid="loginform_login"]') ||
document.querySelector('[data-itemid="username_button"]');
if (!loginSpan) return;
var self = this;
var currentPath = localStorage.getItem('user-selected-theme-path') || '';
var wrapper = document.createElement('div');
wrapper.style.cssText = 'display:inline-flex; align-items:center; margin-right:10px; z-index:9999;';
var select = document.createElement('select');
select.id = this.selectId;
select.style.cssText = 'padding:4px; border-radius:4px; border:1px solid #ccc; color:#333; background:#fff; font-size:12px; height:30px; cursor:pointer;';
this.availableThemes.forEach(function(t) {
var opt = document.createElement('option');
opt.value = t.path;
opt.innerHTML = t.name;
// Επιλογή αν το τρέχον path (ακόμα και αν είναι -night) ανήκει σε αυτό το θέμα
if (currentPath.replace('-night/', '/') === t.path) opt.selected = true;
select.appendChild(opt);
});
var toggle = document.createElement('button');
toggle.id = this.toggleId;
var isNight = currentPath.indexOf('-night/') !== -1 || currentPath.indexOf('darkly') !== -1 || currentPath.indexOf('cyborg') !== -1;
toggle.innerHTML = isNight ? '☀️' : '