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
+319 -1
View File
@@ -1 +1,319 @@
/* Mythic Oracle — styles */ /* Mythic Oracle — design tokens, layout, typography, sidebar */
:root {
--font-heading: 'Cormorant Garamond', serif;
--font-body: 'Lora', serif;
--font-mono: 'JetBrains Mono', monospace;
--sidebar-width: 220px;
--sidebar-width-collapsed: 52px;
--transition-speed: 0.2s;
}
[data-theme="dark"] {
--bg: #121110;
--bg-elevated: #181613;
--bg-hover: #221f1a;
--border: #2c2822;
--text-primary: #e8e2d6;
--text-secondary: #b0a795;
--text-muted: #756c5c;
--accent: #c9a35c;
--accent-hover: #ddb876;
--accent-text: #121110;
}
[data-theme="light"] {
--bg: #f5f1ea;
--bg-elevated: #ece4d4;
--bg-hover: #e1d6c0;
--border: #d6c9ac;
--text-primary: #2b2620;
--text-secondary: #55503f;
--text-muted: #8a8168;
--accent: #a3822f;
--accent-hover: #8c6f27;
--accent-text: #f5f1ea;
}
* ,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-body);
background: var(--bg);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
font-weight: 600;
color: var(--text-primary);
margin: 0 0 0.5em;
}
.app {
display: flex;
height: 100vh;
overflow: hidden;
}
/* --- Sidebar --- */
.sidebar {
display: flex;
flex-direction: column;
flex-shrink: 0;
width: var(--sidebar-width);
background: var(--bg-elevated);
border-right: 1px solid var(--border);
transition: width var(--transition-speed) ease;
overflow: hidden;
}
.sidebar.collapsed {
width: var(--sidebar-width-collapsed);
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 16px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 10px;
overflow: hidden;
}
.sidebar-logo {
flex-shrink: 0;
width: 20px;
height: 20px;
color: var(--accent);
}
.sidebar-title {
font-family: var(--font-heading);
font-size: 1.3rem;
font-weight: 600;
color: var(--accent);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar.collapsed .sidebar-title {
display: none;
}
.sidebar-collapse-toggle {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
}
.sidebar-collapse-toggle:hover {
color: var(--accent);
}
.sidebar-collapse-toggle svg {
width: 16px;
height: 16px;
transition: transform var(--transition-speed) ease;
}
.sidebar.collapsed .sidebar-collapse-toggle svg {
transform: rotate(180deg);
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 8px 0;
}
.nav-section {
margin-bottom: 12px;
}
.nav-section-label {
padding: 8px 16px 4px;
font-family: var(--font-body);
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
white-space: nowrap;
}
.sidebar.collapsed .nav-section-label {
display: none;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 8px 16px;
border: none;
border-left: 2px solid transparent;
background: none;
color: var(--text-secondary);
font-family: var(--font-body);
font-size: 0.9rem;
text-align: left;
cursor: pointer;
transition: background var(--transition-speed) ease, color var(--transition-speed) ease;
}
.nav-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.nav-item.active {
background: var(--bg-hover);
color: var(--accent);
border-left-color: var(--accent);
}
.nav-icon {
flex-shrink: 0;
width: 18px;
height: 18px;
}
.nav-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar.collapsed .nav-item {
justify-content: center;
padding: 10px 0;
border-left: none;
border-right: 2px solid transparent;
}
.sidebar.collapsed .nav-item.active {
border-right-color: var(--accent);
}
.sidebar.collapsed .nav-label {
display: none;
}
/* --- Chaos factor --- */
.chaos-factor {
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid var(--border);
}
.chaos-factor-label {
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
white-space: nowrap;
}
.sidebar.collapsed .chaos-factor-label {
display: none;
}
.chaos-factor-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.sidebar.collapsed .chaos-factor-controls {
flex-direction: column-reverse;
}
.chaos-btn {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 24px;
height: 24px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 0.9rem;
cursor: pointer;
transition: border-color var(--transition-speed) ease, color var(--transition-speed) ease;
}
.chaos-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.chaos-factor-value {
min-width: 1.5em;
font-family: var(--font-mono);
font-size: 1.1rem;
color: var(--accent);
text-align: center;
}
/* --- Main content --- */
.main-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.view {
display: none;
}
.view.active {
display: block;
}
+132
View File
@@ -12,7 +12,139 @@
<link rel="stylesheet" href="css/styles.css"> <link rel="stylesheet" href="css/styles.css">
</head> </head>
<body> <body>
<div class="app">
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-brand">
<svg class="sidebar-logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 3l1.8 4.6L18 9l-4.2 1.4L12 15l-1.8-4.6L6 9l4.2-1.4L12 3z"/>
</svg>
<span class="sidebar-title">Mythic Oracle</span>
</div>
<button class="sidebar-collapse-toggle" id="collapseToggle" aria-label="Collapse sidebar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M15 6l-6 6 6 6"/>
</svg>
</button>
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-label">Oracle</div>
<button class="nav-item active" data-view="oracle">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="11" r="7"/>
<path d="M6 20h12"/>
</svg>
<span class="nav-label">Oracle</span>
</button>
<button class="nav-item" data-view="meaning">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 4h16v12H8l-4 4V4z"/>
</svg>
<span class="nav-label">Meaning</span>
</button>
<button class="nav-item" data-view="une">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 6h16M4 12h16M4 18h10"/>
</svg>
<span class="nav-label">UNE</span>
</button>
</div>
<div class="nav-section">
<div class="nav-section-label">Tools</div>
<button class="nav-item" data-view="dice">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="4" y="4" width="16" height="16" rx="3"/>
<circle cx="8.5" cy="8.5" r="0.75" fill="currentColor" stroke="none"/>
<circle cx="15.5" cy="8.5" r="0.75" fill="currentColor" stroke="none"/>
<circle cx="12" cy="12" r="0.75" fill="currentColor" stroke="none"/>
<circle cx="8.5" cy="15.5" r="0.75" fill="currentColor" stroke="none"/>
<circle cx="15.5" cy="15.5" r="0.75" fill="currentColor" stroke="none"/>
</svg>
<span class="nav-label">Dice</span>
</button>
<button class="nav-item" data-view="tables">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="1"/>
<path d="M3 9h18M3 15h18M9 3v18M15 3v18"/>
</svg>
<span class="nav-label">Tables</span>
</button>
</div>
<div class="nav-section">
<div class="nav-section-label">Campaign</div>
<button class="nav-item" data-view="threads">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 6h2M4 12h2M4 18h2M9 6h11M9 12h11M9 18h11"/>
</svg>
<span class="nav-label">Threads</span>
</button>
<button class="nav-item" data-view="npcs">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="8" r="4"/>
<path d="M4 20c0-4 4-6 8-6s8 2 8 6"/>
</svg>
<span class="nav-label">NPCs</span>
</button>
<button class="nav-item" data-view="notes">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M6 3h9l3 3v15H6z"/>
<path d="M9 8h8M9 12h8M9 16h5"/>
</svg>
<span class="nav-label">Notes</span>
</button>
<button class="nav-item" data-view="character">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 3l7 3v6c0 5-3.5 8-7 9-3.5-1-7-4-7-9V6z"/>
</svg>
<span class="nav-label">Character</span>
</button>
</div>
<div class="nav-section">
<div class="nav-section-label">Settings</div>
<button class="nav-item" data-view="campaign-management">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="3"/>
<path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"/>
</svg>
<span class="nav-label">Campaign Management</span>
</button>
<button class="nav-item nav-action" id="themeToggle" aria-label="Toggle light and dark theme">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5z"/>
</svg>
<span class="nav-label">Theme Toggle</span>
</button>
</div>
</nav>
<div class="chaos-factor">
<div class="chaos-factor-label">Chaos Factor</div>
<div class="chaos-factor-controls">
<button class="chaos-btn" id="chaosMinus" aria-label="Decrease chaos factor">&minus;</button>
<span class="chaos-factor-value" id="chaosValue">5</span>
<button class="chaos-btn" id="chaosPlus" aria-label="Increase chaos factor">+</button>
</div>
</div>
</aside>
<main class="main-content">
<div class="view active" id="view-oracle"></div>
<div class="view" id="view-meaning"></div>
<div class="view" id="view-une"></div>
<div class="view" id="view-dice"></div>
<div class="view" id="view-tables"></div>
<div class="view" id="view-threads"></div>
<div class="view" id="view-npcs"></div>
<div class="view" id="view-notes"></div>
<div class="view" id="view-character"></div>
<div class="view" id="view-campaign-management"></div>
</main>
</div>
</body> </body>
<script src="js/app.js" type="module"></script> <script src="js/app.js" type="module"></script>
</html> </html>
+84
View File
@@ -1 +1,85 @@
// Mythic Oracle — entry, sidebar routing, campaign context // 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);