Initial commit — Meridian OS browser desktop demo

This commit is contained in:
2026-04-19 23:18:55 +00:00
commit 8e30dd3597
30 changed files with 6277 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
import State from '../core/state.js';
import { Bus } from '../core/bus.js';
function renderAchievements(achievements) {
const container = document.getElementById('achievements-list');
if (!container) return;
if (!achievements || achievements.length === 0) {
container.innerHTML = '<div class="achievements-empty">No achievements yet</div>';
return;
}
// Show last 3
const last3 = achievements.slice(-3).reverse();
container.innerHTML = '';
for (const achId of last3) {
const item = document.createElement('div');
item.className = 'achievement-item';
item.innerHTML = `
<div class="achievement-icon">${getInitial(achId)}</div>
<div class="achievement-info">
<span class="achievement-name">${escapeHtml(achId)}</span>
<span class="achievement-game">Unlocked</span>
</div>
`;
container.appendChild(item);
}
}
function getInitial(id) {
return id.charAt(0).toUpperCase();
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function showToast({ id, title, desc }) {
const layer = document.getElementById('toast-layer');
if (!layer) return;
const toast = document.createElement('div');
toast.className = 'toast';
toast.innerHTML = `
<div>
<div class="toast-title">Achievement Unlocked</div>
<div class="toast-desc">${escapeHtml(title || desc || id)}</div>
</div>
<button class="toast-close" aria-label="Dismiss">&times;</button>
`;
layer.appendChild(toast);
// Close button
toast.querySelector('.toast-close').addEventListener('click', () => removeToast(toast));
// Auto dismiss
setTimeout(() => removeToast(toast), 4000);
}
function removeToast(toast) {
if (!toast.parentNode) return;
toast.classList.add('toast-removing');
setTimeout(() => toast.remove(), 300);
}
function init() {
State.subscribe('achievements', renderAchievements);
// Listen for achievement unlocks
Bus.on('achievement:unlocked', showToast);
}
init();

33
js/widgets/active-apps.js Normal file
View File

@@ -0,0 +1,33 @@
import State from '../core/state.js';
import MeridianWM from '../core/wm.js';
import Registry from '../core/registry.js';
function renderApps(apps) {
const container = document.getElementById('active-apps-list');
if (!container) return;
if (!apps || apps.length === 0) {
container.innerHTML = '<div class="active-apps-empty">No apps open</div>';
return;
}
container.innerHTML = '';
for (const win of apps) {
const btn = document.createElement('button');
btn.className = 'active-app-chip';
btn.setAttribute('aria-label', `Focus ${win.title}`);
btn.textContent = win.title;
btn.addEventListener('click', () => {
MeridianWM.focusWindow(win.id);
});
container.appendChild(btn);
}
}
function init() {
State.subscribe('windows', renderApps);
}
init();

39
js/widgets/clock.js Normal file
View File

@@ -0,0 +1,39 @@
import State from '../core/state.js';
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
function formatTime(date) {
let h = date.getHours();
const m = date.getMinutes().toString().padStart(2, '0');
const ampm = h >= 12 ? 'PM' : 'AM';
h = h % 12 || 12;
return `${h}:${m} ${ampm}`;
}
function formatDate(date) {
return `${DAYS[date.getDay()]}, ${MONTHS[date.getMonth()]} ${date.getDate()}`;
}
function updateClock() {
const now = new Date();
const timeEl = document.getElementById('clock-time');
const dateEl = document.getElementById('clock-date');
if (timeEl) timeEl.textContent = formatTime(now);
if (dateEl) dateEl.textContent = formatDate(now);
// Update login screen time
const loginTimeEl = document.getElementById('login-time');
if (loginTimeEl) loginTimeEl.textContent = formatTime(now);
// Update taskbar time
const statusTimeEl = document.getElementById('status-time');
if (statusTimeEl) statusTimeEl.textContent = formatTime(now);
}
function init() {
updateClock();
setInterval(updateClock, 1000);
}
init();

138
js/widgets/wallpaper.js Normal file
View File

@@ -0,0 +1,138 @@
import State from '../core/state.js';
import { Bus } from '../core/bus.js';
const WALLPAPERS = {
graphite: {
name: 'Graphite',
gradient: 'radial-gradient(ellipse at center, #1e2126 0%, #0f1114 100%)'
},
ember: {
name: 'Ember',
gradient: 'linear-gradient(135deg, #1a1d21 0%, #2a1f15 40%, #d4a24c33 100%)'
},
tide: {
name: 'Tide',
gradient: 'linear-gradient(160deg, #0a1628 0%, #0f2847 40%, #14425a 70%, #1a5c6e 100%)'
},
vellum: {
name: 'Vellum',
gradient: 'linear-gradient(145deg, #1e1c1a 0%, #2a2520 50%, #332e28 100%)'
},
eclipse: {
name: 'Eclipse',
gradient: 'linear-gradient(160deg, #0a0a0f 0%, #150f20 50%, #1a1030 100%)'
},
terminal: {
name: 'Terminal',
gradient: 'radial-gradient(ellipse at 20% 50%, #0a1a0a 0%, #0f1410 40%, #0a0f0a 100%)'
}
};
let currentWp = null;
let applying = false;
function applyWallpaper(id) {
if (applying) return;
const wp = WALLPAPERS[id];
if (!wp) return;
const layer = document.getElementById('wallpaper-layer');
if (!layer) return;
applying = true;
// Crossfade
const overlay = document.createElement('div');
overlay.style.position = 'absolute';
overlay.style.inset = '0';
overlay.style.background = wp.gradient;
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 250ms ease';
overlay.style.zIndex = '1';
layer.appendChild(overlay);
requestAnimationFrame(() => {
overlay.style.opacity = '1';
if (currentWp && currentWp !== overlay) {
const old = currentWp;
old.style.opacity = '0';
setTimeout(() => old.remove(), 300);
}
currentWp = overlay;
});
if (State.get('wallpaper') !== id) {
State.set('wallpaper', id);
}
applying = false;
// Update picker swatches
document.querySelectorAll('.wallpaper-swatch').forEach(s => {
s.classList.toggle('active', s.dataset.id === id);
});
}
function initPicker() {
const picker = document.getElementById('wallpaper-picker');
const grid = document.getElementById('wallpaper-picker-grid');
if (!picker || !grid) return;
// Populate swatches
for (const [id, wp] of Object.entries(WALLPAPERS)) {
const swatch = document.createElement('div');
swatch.className = 'wallpaper-swatch';
swatch.dataset.id = id;
swatch.setAttribute('role', 'button');
swatch.setAttribute('aria-label', `Select ${wp.name} wallpaper`);
swatch.style.background = wp.gradient;
swatch.innerHTML = `<span class="wallpaper-swatch-label">${wp.name}</span>`;
swatch.addEventListener('click', () => applyWallpaper(id));
grid.appendChild(swatch);
}
// Apply saved wallpaper
const saved = State.get('wallpaper');
if (saved && WALLPAPERS[saved]) {
applyWallpaper(saved);
}
// Toggle button in status bar
const toggleBtn = document.getElementById('wallpaper-toggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
picker.classList.toggle('hidden');
});
}
// Close picker on backdrop click
picker.addEventListener('click', (e) => {
if (e.target === picker) {
picker.classList.add('hidden');
}
});
// Close on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !picker.classList.contains('hidden')) {
picker.classList.add('hidden');
}
});
}
function init() {
// Listen for state changes to wallpaper (external triggers only; applying flag prevents recursion)
State.subscribe('wallpaper', (id) => {
if (id && WALLPAPERS[id]) {
applyWallpaper(id);
}
});
// Apply saved or default
const saved = State.get('wallpaper');
const wpId = (saved && WALLPAPERS[saved]) ? saved : 'graphite';
applyWallpaper(wpId);
initPicker();
}
init();