diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml
new file mode 100644
index 0000000000..f4ea1a75fd
--- /dev/null
+++ b/.github/workflows/pages.yaml
@@ -0,0 +1,69 @@
+name: Deploy to GitHub Pages
+
+on:
+ push:
+ branches: ["master"]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Create redirect index.html
+ run: |
+ cat > index.html << 'EOF'
+
+
+
+
+
+
+ MagicMirror² Demo
+
+
+
+
+ Redirecting to MagicMirror² Demo...
+
+
+ EOF
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v4
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: "."
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/docs/demo/README.md b/docs/demo/README.md
new file mode 100644
index 0000000000..af5947ce81
--- /dev/null
+++ b/docs/demo/README.md
@@ -0,0 +1,128 @@
+# MagicMirror² Browser Demo
+
+A browser-based demo version of [MagicMirror²](https://github.com/MagicMirrorOrg/MagicMirror).
+
+## Live Demo
+
+**Try it now:** [https://magicmirrororg.github.io/MagicMirror/docs/demo/](https://magicmirrororg.github.io/MagicMirror/docs/demo/)
+
+## What makes this special?
+
+This demo runs **real MagicMirror² code** from the `master` branch in your browser. It's not a simplified recreation - it uses the actual:
+
+- Core architecture (`Module.register()`, `Class.extend()`)
+- Original default modules (clock, weather, compliments, calendar, newsfeed)
+- Real CSS styling
+- Authentic module system and lifecycle
+
+**The only difference:** Server components are mocked with demo data instead of making real API calls.
+
+This means you're seeing the real MagicMirror² experience, just without needing a server or API keys!
+
+## Running locally
+
+```bash
+# IMPORTANT: Server must run from the MagicMirror root directory!
+cd /path/to/MagicMirror
+
+# With Python 3
+python -m http.server 8080
+
+# Or with Node.js
+npx http-server -p 8080
+
+# Open in browser
+open http://localhost:8080/docs/demo/
+```
+
+**Why from root?** The demo uses real files with relative paths (`../../js/`, `../../modules/`) - the server must run from the repository root.
+
+## Structure
+
+```
+demo/
+├── index.html # Loads real modules
+├── config.js # Demo configuration
+├── css/
+│ └── demo.css # Demo banner styling only
+└── js/
+ ├── demo-mocks.js # Mocks for Log, Translator, Socket.io
+ ├── demo-loader.js # Overrides API calls with mock data
+ └── demo-main.js # Initializes MM like the original
+```
+
+## How it works
+
+1. **index.html** loads the real MagicMirror core files:
+ - `js/class.js` - Base class system
+ - `js/module.js` - Module architecture
+ - Original modules from `modules/default/`
+
+2. **demo-mocks.js** creates replacements for server components:
+ - `window.Log` for logging
+ - `window.Translator` for translations
+ - `window.MM` for module manager
+ - Mock data for weather, calendar, news
+
+3. **demo-loader.js** extends modules during registration:
+ - Intercepts `Module.register()`
+ - Overrides `start()` methods
+ - Replaces API calls with mock data
+
+4. **demo-main.js** starts the system:
+ - Reads `config.modules`
+ - Creates module instances
+ - Inserts DOM like the original
+
+## Customization
+
+### Change configuration
+
+Edit [config.js](config.js):
+
+```javascript
+modules: [
+ {
+ module: "clock",
+ position: "top_left",
+ config: {
+ /* ... */
+ }
+ }
+ // More modules...
+];
+```
+
+### Change mock data
+
+Edit [js/demo-mocks.js](js/demo-mocks.js):
+
+```javascript
+const mockWeatherData = {
+ current: {
+ temperature: 8
+ // ...
+ }
+};
+```
+
+## Limitations
+
+As a pure browser demo:
+
+- ❌ No real API calls (CORS, API keys)
+- ❌ No node_helper.js modules
+- ❌ No third-party modules (but easy to add!)
+- ❌ No Electron features
+
+## Adding new modules
+
+1. Add script tag in [index.html](index.html):
+
+```html
+
+```
+
+2. Add module to config in [config.js](config.js)
+
+3. If API calls needed: Add mock function in [demo-loader.js](js/demo-loader.js)
diff --git a/docs/demo/config.js b/docs/demo/config.js
new file mode 100644
index 0000000000..d1d548d790
--- /dev/null
+++ b/docs/demo/config.js
@@ -0,0 +1,68 @@
+/* MagicMirror² Demo Configuration */
+let config = {
+ language: "en",
+ timeFormat: 12,
+ units: "metric",
+ basePath: "/",
+
+ modules: [
+ {
+ module: "clock",
+ position: "top_left",
+ config: {
+ displaySeconds: true,
+ showDate: true,
+ showWeek: true,
+ dateFormat: "dddd, MMMM Do, YYYY"
+ }
+ },
+ {
+ module: "compliments",
+ position: "lower_third",
+ config: {
+ compliments: {
+ morning: ["Good morning!", "Enjoy your day!", "Nice to see you!"],
+ afternoon: ["Hello!", "Looking good!", "Beautiful afternoon!"],
+ evening: ["Good evening!", "Have a nice evening!", "Time to relax!"]
+ }
+ }
+ },
+ {
+ module: "weather",
+ position: "top_right",
+ config: {
+ weatherProvider: "openweathermap",
+ type: "current",
+ location: "Berlin",
+ locationID: "2950159",
+ apiKey: "DEMO_KEY"
+ }
+ },
+ {
+ module: "calendar",
+ position: "top_left",
+ header: "Calendar",
+ config: {
+ maximumEntries: 5,
+ calendars: []
+ }
+ },
+ {
+ module: "newsfeed",
+ position: "bottom_bar",
+ config: {
+ feeds: [
+ {
+ title: "Demo News",
+ url: "about:blank"
+ }
+ ],
+ showSourceTitle: true,
+ showPublishDate: false
+ }
+ }
+ ]
+};
+
+/*************** DO NOT EDIT THE LINE BELOW ***************/
+if (typeof module !== "undefined") { module.exports = config; }
diff --git a/docs/demo/css/demo.css b/docs/demo/css/demo.css
new file mode 100644
index 0000000000..3c150097fb
--- /dev/null
+++ b/docs/demo/css/demo.css
@@ -0,0 +1,47 @@
+/* Demo Banner */
+#demo-banner {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
+ color: #fff;
+ padding: 8px 20px;
+ text-align: center;
+ z-index: 10000;
+ font-size: 14px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ border-bottom: 1px solid #333;
+}
+
+#demo-banner a {
+ color: #4fc3f7;
+ text-decoration: none;
+}
+
+#demo-banner a:hover {
+ text-decoration: underline;
+}
+
+#demo-banner button {
+ background: none;
+ border: none;
+ color: #fff;
+ font-size: 20px;
+ cursor: pointer;
+ padding: 0 5px;
+ margin-left: 10px;
+ opacity: 0.7;
+}
+
+#demo-banner button:hover {
+ opacity: 1;
+}
+
+/* Adjust body margin for banner */
+body {
+ margin-top: calc(var(--gap-body-top) + 40px);
+}
diff --git a/docs/demo/index.html b/docs/demo/index.html
new file mode 100644
index 0000000000..e31d392773
--- /dev/null
+++ b/docs/demo/index.html
@@ -0,0 +1,107 @@
+
+
+
+
+
+ MagicMirror² Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🪞 MagicMirror² Demo - GitHub
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/demo/js/demo-loader.js b/docs/demo/js/demo-loader.js
new file mode 100644
index 0000000000..c443b8488f
--- /dev/null
+++ b/docs/demo/js/demo-loader.js
@@ -0,0 +1,263 @@
+/* MagicMirror² Demo - Module Loader
+ * Loads and initializes real MagicMirror modules for the demo mode
+ */
+
+(function () {
+ "use strict";
+
+ // Extend Module class with demo-specific overrides
+ const originalModuleRegister = Module.register;
+
+ Module.register = function (name, moduleDefinition) {
+ console.log("Registering module:", name);
+
+ // Override specific methods for demo mode
+ const originalStart = moduleDefinition.start;
+ moduleDefinition.start = async function () {
+ console.log(`Starting module: ${name}`);
+
+ // Call original start if exists
+ if (originalStart) {
+ await originalStart.call(this);
+ }
+
+ // Module-specific demo setups AFTER starting (so weatherProvider exists)
+ if (name === "weather") {
+ setupWeatherDemo.call(this);
+ } else if (name === "calendar") {
+ setupCalendarDemo.call(this);
+ } else if (name === "newsfeed") {
+ setupNewsfeedDemo.call(this);
+ }
+ };
+
+ // Call original register
+ return originalModuleRegister.call(this, name, moduleDefinition);
+ };
+
+ // ============================================
+ // Weather Demo Setup
+ // ============================================
+ /**
+ *
+ */
+ function setupWeatherDemo () {
+ const self = this;
+ const provider = this.weatherProvider;
+
+ if (!provider) {
+ console.error("Weather provider not initialized");
+ return;
+ }
+
+ console.log("Weather: Setting up mock data");
+
+ // Override getDom to render without nunjucks template
+ this.getDom = function () {
+ const wrapper = document.createElement("div");
+
+ const current = this.weatherProvider?.currentWeather();
+ if (!current) {
+ wrapper.innerHTML = "Lade Wetter...";
+ return wrapper;
+ }
+
+ // Temperature display
+ const tempWrapper = document.createElement("div");
+ tempWrapper.className = "large light";
+
+ const icon = document.createElement("span");
+ icon.className = `wi weathericon wi-${current.weatherType}`;
+ tempWrapper.appendChild(icon);
+
+ const temp = document.createElement("span");
+ temp.className = "bright";
+ temp.innerHTML = ` ${current.temperature.toFixed(1)}°`;
+ tempWrapper.appendChild(temp);
+
+ wrapper.appendChild(tempWrapper);
+
+ // Additional info
+ if (!this.config.onlyTemp) {
+ const detailsWrapper = document.createElement("div");
+ detailsWrapper.className = "normal medium";
+
+ // Wind
+ const wind = document.createElement("span");
+ wind.innerHTML = ` ${current.windSpeed.toFixed(0)} m/s`;
+ detailsWrapper.appendChild(wind);
+
+ // Humidity
+ if (current.humidity) {
+ const humidity = document.createElement("span");
+ humidity.innerHTML = ` ${current.humidity}%`;
+ detailsWrapper.appendChild(humidity);
+ }
+
+ wrapper.appendChild(detailsWrapper);
+ }
+
+ return wrapper;
+ };
+
+ // Override fetch methods to use mock data
+ provider.fetchCurrentWeather = function () {
+ console.log("Weather: Using mock current weather data");
+
+ // Use the real WeatherObject class
+ const current = new WeatherObject();
+ current.date = moment();
+ current.temperature = mockData.weather.current.temperature;
+ current.feelsLikeTemp = mockData.weather.current.feelsLike;
+ current.weatherType = mockData.weather.current.weatherType;
+ current.humidity = mockData.weather.current.humidity;
+ current.windSpeed = mockData.weather.current.windSpeed;
+ current.windFromDirection = mockData.weather.current.windDirection;
+ current.sunrise = moment(mockData.weather.current.sunrise);
+ current.sunset = moment(mockData.weather.current.sunset);
+
+ this.setCurrentWeather(current);
+ this.updateAvailable();
+ };
+
+ provider.fetchWeatherForecast = function () {
+ console.log("Weather: Using mock forecast data");
+
+ const forecasts = mockData.weather.forecast.map((f) => {
+ const fc = new WeatherObject();
+ fc.date = moment(f.date);
+ fc.minTemperature = f.tempMin;
+ fc.maxTemperature = f.tempMax;
+ fc.weatherType = f.weatherType;
+ fc.precipitationAmount = f.precipitation;
+ return fc;
+ });
+
+ this.setWeatherForecast(forecasts);
+ this.updateAvailable();
+ };
+
+ provider.fetchWeatherHourly = function () {
+ console.log("Weather: Mock hourly data not implemented");
+ };
+ }
+
+ // ============================================
+ // Calendar Demo Setup
+ // ============================================
+ /**
+ *
+ */
+ function setupCalendarDemo () {
+ const self = this;
+
+ // Mark as loaded
+ this.loaded = true;
+
+ // Override addCalendar to prevent actual fetching
+ this.addCalendar = function () {};
+
+ // Simulate calendar events
+ setTimeout(() => {
+ const events = mockData.calendar.map((e) => ({
+ title: e.title,
+ startDate: e.startDate,
+ endDate: e.endDate || e.startDate,
+ fullDayEvent: e.fullDayEvent || false,
+ location: "",
+ geo: null,
+ description: "",
+ today: e.startDate >= Date.now() && e.startDate < Date.now() + 86400000
+ }));
+
+ // Sort by start date
+ events.sort((a, b) => a.startDate - b.startDate);
+
+ // Set events directly on the module in the expected format
+ self.calendarData = {
+ mock_calendar: {
+ events: events
+ }
+ };
+
+ // Broadcast calendar events
+ self.broadcastEvents(events);
+
+ // Update display
+ self.updateDom(300);
+ }, 500);
+ }
+
+ // ============================================
+ // Newsfeed Demo Setup
+ // ============================================
+ /**
+ *
+ */
+ function setupNewsfeedDemo () {
+ const self = this;
+ let currentIndex = 0;
+
+ // Override getDom to render without nunjucks template
+ this.getDom = function () {
+ const wrapper = document.createElement("div");
+ wrapper.className = "newsfeed";
+
+ if (!this.newsItems || this.newsItems.length === 0) {
+ wrapper.innerHTML = "Loading news...";
+ return wrapper;
+ }
+
+ const item = this.newsItems[this.activeItem || 0];
+ if (!item) {
+ return wrapper;
+ }
+
+ const titleWrapper = document.createElement("div");
+ titleWrapper.className = "newsfeed-title";
+
+ if (this.config.showSourceTitle && item.sourceTitle) {
+ const source = document.createElement("span");
+ source.className = "newsfeed-source";
+ source.innerHTML = `${item.sourceTitle}: `;
+ titleWrapper.appendChild(source);
+ }
+
+ const title = document.createElement("span");
+ title.innerHTML = item.title;
+ titleWrapper.appendChild(title);
+
+ wrapper.appendChild(titleWrapper);
+
+ if (this.config.showDescription && item.description) {
+ const desc = document.createElement("div");
+ desc.className = "newsfeed-desc";
+ desc.innerHTML = item.description;
+ wrapper.appendChild(desc);
+ }
+
+ return wrapper;
+ };
+
+ // Override startFetch
+ this.startFetch = function () {
+ console.log("Newsfeed: Using mock data");
+ };
+
+ // Simulate news items cycling
+ const updateNews = () => {
+ const item = mockData.news[currentIndex % mockData.news.length];
+ self.newsItems = [item];
+ self.activeItem = 0;
+ self.loaded = true;
+ self.updateDom(self.config.animationSpeed || 1000);
+
+ currentIndex++;
+ setTimeout(updateNews, self.config.updateInterval || 10000);
+ };
+
+ setTimeout(updateNews, 500);
+ }
+
+ console.log("Demo loader initialized");
+}());
diff --git a/docs/demo/js/demo-main.js b/docs/demo/js/demo-main.js
new file mode 100644
index 0000000000..b91c423a26
--- /dev/null
+++ b/docs/demo/js/demo-main.js
@@ -0,0 +1,198 @@
+/* MagicMirror² Demo - Main Application
+ * Initializes the demo with real MagicMirror modules
+ */
+
+(function () {
+ "use strict";
+
+ console.log("MagicMirror² Demo starting...");
+
+ // Position mapping (convert underscores to spaces for CSS classes)
+ const positionMap = {
+ top_bar: "top bar",
+ top_left: "top left",
+ top_center: "top center",
+ top_right: "top right",
+ upper_third: "upper third",
+ middle_center: "middle center",
+ lower_third: "lower third",
+ bottom_bar: "bottom bar",
+ bottom_left: "bottom left",
+ bottom_center: "bottom center",
+ bottom_right: "bottom right",
+ fullscreen_above: "fullscreen above",
+ fullscreen_below: "fullscreen below"
+ };
+
+ // Helper: Get container element for position
+ /**
+ *
+ * @param position
+ */
+ function getContainer (position) {
+ const mappedPosition = positionMap[position] || position;
+ const classes = mappedPosition.replace(/_/g, " ");
+ const regions = document.querySelectorAll(".region");
+
+ for (const region of regions) {
+ if (region.className.includes(classes)) {
+ const container = region.querySelector(".container");
+ if (container) return container;
+ }
+ }
+
+ console.warn(`Container not found for position: ${position}`);
+ return null;
+ }
+
+ // Helper: Create module DOM wrapper
+ /**
+ *
+ * @param module
+ */
+ function createModuleWrapper (module) {
+ const wrapper = document.createElement("div");
+ wrapper.id = module.identifier;
+ wrapper.className = `module ${module.name}`;
+
+ if (module.data.classes) {
+ wrapper.className += ` ${module.data.classes}`;
+ }
+
+ // Add header
+ const header = document.createElement("header");
+ header.className = "module-header";
+ wrapper.appendChild(header);
+
+ // Add content
+ const content = document.createElement("div");
+ content.className = "module-content";
+ wrapper.appendChild(content);
+
+ return wrapper;
+ }
+
+ // Helper: Update module DOM
+ /**
+ *
+ * @param module
+ */
+ async function updateModuleDom (module) {
+ const wrapper = document.getElementById(module.identifier);
+ if (!wrapper) return;
+
+ const content = wrapper.querySelector(".module-content");
+ const header = wrapper.querySelector(".module-header");
+
+ // Update header
+ if (typeof module.getHeader === "function") {
+ const headerText = module.getHeader();
+ header.innerHTML = headerText || "";
+ header.style.display = headerText ? "block" : "none";
+ }
+
+ // Update content
+ try {
+ const dom = await module.getDom();
+ content.innerHTML = "";
+ content.appendChild(dom);
+ } catch (error) {
+ console.error(`Error updating module ${module.name}:`, error);
+ }
+ }
+
+ // Initialize modules
+ /**
+ *
+ */
+ async function initModules () {
+ console.log("Initializing modules from config...");
+
+ const moduleInstances = [];
+ let moduleId = 0;
+
+ for (const moduleConfig of config.modules) {
+ const moduleName = moduleConfig.module;
+ const moduleData = {
+ ...moduleConfig,
+ name: moduleName,
+ identifier: `module_${moduleId}_${moduleName}`,
+ hidden: false,
+ index: moduleId,
+ classes: moduleConfig.classes || `${moduleName}`
+ };
+
+ console.log(`Creating module: ${moduleName}`);
+
+ // Check if module is registered
+ if (typeof Module === "undefined" || !Module.definitions || !Module.definitions[moduleName]) {
+ console.warn(`Module ${moduleName} not registered yet, skipping for now`);
+ continue;
+ }
+
+ // Create module instance (Module.create already returns an instance, not a class!)
+ const instance = Module.create(moduleName);
+ if (!instance) {
+ console.error(`Failed to create module: ${moduleName}`);
+ continue;
+ }
+
+ instance.setData(moduleData);
+ instance.setConfig(moduleConfig.config || {});
+
+ // Store instance
+ moduleInstances.push(instance);
+ MM.modules.push(instance);
+
+ // Create and insert DOM
+ const container = getContainer(moduleConfig.position);
+ if (container) {
+ const wrapper = createModuleWrapper(instance);
+ container.appendChild(wrapper);
+
+ // Override updateDom for this instance
+ instance.updateDom = function (speed) {
+ updateModuleDom(instance);
+ };
+ }
+
+ moduleId++;
+ }
+
+ // Start all modules
+ console.log("Starting modules...");
+ for (const module of moduleInstances) {
+ try {
+ if (typeof module.start === "function") {
+ await module.start();
+ }
+ await updateModuleDom(module);
+ } catch (error) {
+ console.error(`Error starting module ${module.name}:`, error);
+ }
+ }
+
+ console.log("MagicMirror² Demo ready! ✨");
+ }
+
+ // Start when DOM is ready
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", () => {
+ // Wait a bit for all modules to register
+ setTimeout(initModules, 100);
+ });
+ } else {
+ setTimeout(initModules, 100);
+ }
+
+ // Keyboard shortcuts
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ const banner = document.getElementById("demo-banner");
+ if (banner) {
+ banner.style.display = banner.style.display === "none" ? "flex" : "none";
+ }
+ }
+ });
+
+}());
diff --git a/docs/demo/js/demo-mocks.js b/docs/demo/js/demo-mocks.js
new file mode 100644
index 0000000000..def0cfc927
--- /dev/null
+++ b/docs/demo/js/demo-mocks.js
@@ -0,0 +1,284 @@
+/* MagicMirror² Demo - Mock Implementations
+ * Simulates server components and APIs for the browser demo
+ */
+
+// ============================================
+// Global Logging Mock
+// ============================================
+window.Log = {
+ info (...args) { console.info("[INFO]", ...args); },
+ log (...args) { console.log("[LOG]", ...args); },
+ error (...args) { console.error("[ERROR]", ...args); },
+ warn (...args) { console.warn("[WARN]", ...args); },
+ debug (...args) { console.debug("[DEBUG]", ...args); }
+};
+
+// ============================================
+// Translator Mock
+// ============================================
+window.Translator = {
+ translations: {
+ LOADING: "Loading …",
+ DAYBEFOREYESTERDAY: "Day Before Yesterday",
+ YESTERDAY: "Yesterday",
+ TODAY: "Today",
+ TOMORROW: "Tomorrow",
+ DAYAFTERTOMORROW: "Day After Tomorrow",
+ RUNNING: "ends in",
+ EMPTY: "No upcoming events.",
+ WEEK: "Week {weekNumber}",
+ WEEK_SHORT: "W{weekNumber}"
+ },
+ coreTranslations: {},
+
+ loadCoreTranslations (lang) {
+ return Promise.resolve();
+ },
+
+ translate (module, key, variables) {
+ // Handle being called with just (key, variables) or (module, key, variables)
+ if (typeof module === "string") {
+ variables = key;
+ key = module;
+ }
+
+ let translation = this.translations[key] || key;
+
+ // Replace variables in translation
+ if (variables && typeof variables === "object") {
+ Object.keys(variables).forEach((varKey) => {
+ translation = translation.replace(`{${varKey}}`, variables[varKey]);
+ });
+ }
+
+ return translation;
+ },
+
+ load (module, file, force, callback) {
+ if (callback) callback();
+ return Promise.resolve();
+ }
+};
+
+// ============================================
+// Nunjucks Mock
+// ============================================
+if (!window.nunjucks) {
+ window.nunjucks = {};
+}
+
+// Mock WebLoader for module.js compatibility
+window.nunjucks.WebLoader = function (baseURL, opts) {
+ this.baseURL = baseURL;
+ this.async = opts && opts.async;
+
+ this.getSource = function (name, callback) {
+ // Return empty template for demo
+ if (callback) {
+ callback(null, { src: "", path: name, noCache: false });
+ }
+ return { src: "", path: name, noCache: false };
+ };
+};
+
+// Mock Environment
+window.nunjucks.Environment = function (loader, opts) {
+ this.loader = loader;
+ this.opts = opts || {};
+ this.filters = {};
+
+ this.addFilter = function (name, fn) {
+ this.filters[name] = fn;
+ return this;
+ };
+
+ this.renderString = function (template, data, callback) {
+ const result = template; // Simple passthrough for demo
+ if (callback) {
+ callback(null, result);
+ return;
+ }
+ return result;
+ };
+
+ this.render = function (name, data, callback) {
+ const result = `Template: ${name}
`; // Mock render
+ if (callback) {
+ callback(null, result);
+ return;
+ }
+ return result;
+ };
+};
+
+// Mock runtime
+window.nunjucks.runtime = {
+ markSafe (val) {
+ return val;
+ }
+};
+
+// Mock configure
+window.nunjucks.configure = function (baseURL, opts) {
+ return new window.nunjucks.Environment(
+ new window.nunjucks.WebLoader(baseURL, opts),
+ opts
+ );
+};
+
+// ============================================
+// Socket.IO Mock
+// ============================================
+window.io = function () {
+ return {
+ on (event, callback) {
+ console.log("Socket event registered:", event);
+ },
+ emit (event, data) {
+ console.log("Socket emit:", event, data);
+ }
+ };
+};
+
+// ============================================
+// MMSocket Mock (for module.js socket() method)
+// ============================================
+window.MMSocket = function (moduleName) {
+ this.moduleName = moduleName;
+ this.notificationCallback = null;
+
+ this.setNotificationCallback = function (callback) {
+ this.notificationCallback = callback;
+ };
+
+ this.sendNotification = function (notification, payload) {
+ console.log(`Socket notification from ${moduleName}:`, notification, payload);
+ // In demo mode, socket notifications are ignored
+ };
+
+ return this;
+};
+
+// ============================================
+// Module Manager Mock
+// ============================================
+window.MM = {
+ modules: [],
+
+ getModules () {
+ return this.modules;
+ },
+
+ sendNotification (notification, payload, sender) {
+ console.log("Notification:", notification, payload);
+ // Broadcast to all modules
+ this.modules.forEach((module) => {
+ if (module !== sender && typeof module.notificationReceived === "function") {
+ module.notificationReceived(notification, payload, sender);
+ }
+ });
+ }
+};
+
+// ============================================
+// Mock Weather Data Provider
+// ============================================
+const mockWeatherData = {
+ current: {
+ temperature: 8,
+ feelsLike: 5,
+ weatherType: "cloudy",
+ humidity: 75,
+ windSpeed: 15,
+ windDirection: 230,
+ sunrise: new Date(Date.now() - 3 * 60 * 60 * 1000),
+ sunset: new Date(Date.now() + 5 * 60 * 60 * 1000)
+ },
+ forecast: [
+ { date: Date.now() + 86400000, tempMin: 4, tempMax: 9, weatherType: "rain", precipitation: 5 },
+ { date: Date.now() + 2 * 86400000, tempMin: 2, tempMax: 7, weatherType: "rain", precipitation: 8 },
+ { date: Date.now() + 3 * 86400000, tempMin: 3, tempMax: 11, weatherType: "cloudy", precipitation: 2 },
+ { date: Date.now() + 4 * 86400000, tempMin: 5, tempMax: 10, weatherType: "cloudy", precipitation: 0 },
+ { date: Date.now() + 5 * 86400000, tempMin: 1, tempMax: 6, weatherType: "snow", precipitation: 10 }
+ ]
+};
+
+// ============================================
+// Mock Calendar Events
+// ============================================
+const mockCalendarEvents = [
+ {
+ title: "Team Meeting",
+ startDate: Date.now() + 2 * 60 * 60 * 1000,
+ endDate: Date.now() + 3 * 60 * 60 * 1000,
+ fullDayEvent: false
+ },
+ {
+ title: "Dentist Appointment",
+ startDate: Date.now() + 5 * 60 * 60 * 1000,
+ endDate: Date.now() + 6 * 60 * 60 * 1000,
+ fullDayEvent: false
+ },
+ {
+ title: "Sarah's Birthday",
+ startDate: Date.now() + 86400000,
+ fullDayEvent: true
+ },
+ {
+ title: "Project Deadline",
+ startDate: Date.now() + 2 * 86400000,
+ endDate: Date.now() + 2 * 86400000 + 60 * 60 * 1000,
+ fullDayEvent: false
+ },
+ {
+ title: "Yoga Class",
+ startDate: Date.now() + 3 * 86400000 + 18 * 60 * 60 * 1000,
+ endDate: Date.now() + 3 * 86400000 + 19 * 60 * 60 * 1000,
+ fullDayEvent: false
+ }
+];
+
+// ============================================
+// Mock News Feed
+// ============================================
+const mockNewsItems = [
+ {
+ title: "New Technologies Revolutionize Daily Life",
+ description: "Innovative solutions are changing our daily habits.",
+ url: "#",
+ pubdate: Date.now() - 2 * 60 * 60 * 1000
+ },
+ {
+ title: "Weather Forecast: Cold Wave Expected",
+ description: "Meteorologists warn of dropping temperatures.",
+ url: "#",
+ pubdate: Date.now() - 4 * 60 * 60 * 1000
+ },
+ {
+ title: "Local Events This Weekend",
+ description: "An overview of events in your city.",
+ url: "#",
+ pubdate: Date.now() - 6 * 60 * 60 * 1000
+ },
+ {
+ title: "Scientists Make Groundbreaking Discovery",
+ description: "New findings could change everything.",
+ url: "#",
+ pubdate: Date.now() - 8 * 60 * 60 * 1000
+ },
+ {
+ title: "Sports: Exciting Games This Weekend",
+ description: "Results from the most important matches.",
+ url: "#",
+ pubdate: Date.now() - 10 * 60 * 60 * 1000
+ }
+];
+
+// Export for use in modules
+window.mockData = {
+ weather: mockWeatherData,
+ calendar: mockCalendarEvents,
+ news: mockNewsItems
+};
+
+console.log("Demo mocks loaded successfully");
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 897777943a..2a929d9617 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -9,7 +9,7 @@ import stylistic from "@stylistic/eslint-plugin";
import vitest from "eslint-plugin-vitest";
export default defineConfig([
- globalIgnores(["config/**", "modules/**/*", "!modules/default/**", "js/positions.js"]),
+ globalIgnores(["config/**", "modules/**/*", "!modules/default/**", "js/positions.js", "docs/demo/**"]),
{
files: ["**/*.js"],
languageOptions: {