Initial commit — Meridian OS browser desktop demo
This commit is contained in:
76
js/widgets/achievements.js
Normal file
76
js/widgets/achievements.js
Normal 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">×</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
33
js/widgets/active-apps.js
Normal 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
39
js/widgets/clock.js
Normal 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
138
js/widgets/wallpaper.js
Normal 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();
|
||||
Reference in New Issue
Block a user