feat: Phase 2 frontend shell

Sidebar with collapsible nav, dark/light theme tokens, and
localStorage-backed state for collapse/theme/active view.
Chaos factor UI is display-only pending Phase 3 backend wiring.
This commit is contained in:
claudecode
2026-06-30 22:39:34 -04:00
parent 3bcd5bc694
commit 2faa168847
3 changed files with 535 additions and 1 deletions
+84
View File
@@ -1 +1,85 @@
// Mythic Oracle — entry, sidebar routing, campaign context
const STORAGE_KEYS = {
sidebarCollapsed: 'mo-sidebar-collapsed',
activeView: 'mo-active-view',
theme: 'mo-theme',
};
const CHAOS_MIN = 1;
const CHAOS_MAX = 9;
function initSidebarCollapse() {
const sidebar = document.getElementById('sidebar');
const toggle = document.getElementById('collapseToggle');
const collapsed = localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === 'true';
sidebar.classList.toggle('collapsed', collapsed);
toggle.addEventListener('click', () => {
const isCollapsed = sidebar.classList.toggle('collapsed');
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, isCollapsed);
});
}
function initNavRouting() {
const navItems = document.querySelectorAll('.nav-item[data-view]');
const views = document.querySelectorAll('.view');
function activateView(viewId) {
navItems.forEach((item) => {
item.classList.toggle('active', item.dataset.view === viewId);
});
views.forEach((view) => {
view.classList.toggle('active', view.id === `view-${viewId}`);
});
}
navItems.forEach((item) => {
item.addEventListener('click', () => {
const viewId = item.dataset.view;
activateView(viewId);
localStorage.setItem(STORAGE_KEYS.activeView, viewId);
});
});
const savedView = localStorage.getItem(STORAGE_KEYS.activeView);
const validView = savedView && document.getElementById(`view-${savedView}`);
activateView(validView ? savedView : 'oracle');
}
function initThemeToggle() {
const toggle = document.getElementById('themeToggle');
const root = document.documentElement;
const savedTheme = localStorage.getItem(STORAGE_KEYS.theme) || 'dark';
root.setAttribute('data-theme', savedTheme);
toggle.addEventListener('click', () => {
const nextTheme = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', nextTheme);
localStorage.setItem(STORAGE_KEYS.theme, nextTheme);
});
}
function initChaosFactorControls() {
const value = document.getElementById('chaosValue');
const minus = document.getElementById('chaosMinus');
const plus = document.getElementById('chaosPlus');
function setValue(next) {
value.textContent = Math.min(CHAOS_MAX, Math.max(CHAOS_MIN, next));
}
minus.addEventListener('click', () => setValue(Number(value.textContent) - 1));
plus.addEventListener('click', () => setValue(Number(value.textContent) + 1));
}
function init() {
initSidebarCollapse();
initNavRouting();
initThemeToggle();
initChaosFactorControls();
}
document.addEventListener('DOMContentLoaded', init);