init
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Directory Browser Styles
|
||||
* Minimal, clean design matching novel-reader aesthetic
|
||||
*/
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background: #faf8f3;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #e0dcd4;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-family: 'Libre Baskerville', Georgia, serif;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 400;
|
||||
color: #8b4513;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
header .path {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
list-style: none;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dir-item {
|
||||
border-bottom: 1px solid #f0ebe3;
|
||||
}
|
||||
|
||||
.dir-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dir-item a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.875rem 1rem;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.dir-item a:hover {
|
||||
background: #faf8f3;
|
||||
}
|
||||
|
||||
.dir-item .icon {
|
||||
font-size: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dir-item .name {
|
||||
font-size: 0.9375rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Folder styles */
|
||||
.dir-item.folder .name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dir-item.parent a {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dir-item.parent a:hover {
|
||||
color: #8b4513;
|
||||
}
|
||||
|
||||
/* Markdown file styles */
|
||||
.dir-item.markdown .name {
|
||||
color: #8b4513;
|
||||
}
|
||||
|
||||
.dir-item.markdown a:hover {
|
||||
background: #f5f0e8;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.dir-item.empty {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e0dcd4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer p {
|
||||
font-size: 0.8125rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom-color: #333;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #d4a574;
|
||||
}
|
||||
|
||||
header .path {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
background: #252525;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dir-item {
|
||||
border-bottom-color: #333;
|
||||
}
|
||||
|
||||
.dir-item a {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.dir-item a:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.dir-item.parent a {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.dir-item.parent a:hover {
|
||||
color: #d4a574;
|
||||
}
|
||||
|
||||
.dir-item.markdown .name {
|
||||
color: #d4a574;
|
||||
}
|
||||
|
||||
.dir-item.markdown a:hover {
|
||||
background: #2d2520;
|
||||
}
|
||||
|
||||
.dir-item.empty {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top-color: #333;
|
||||
}
|
||||
|
||||
footer p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.dir-item a {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.dir-item .icon {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.dir-item .name {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
BIN
.opencode/skills/markdown-novel-viewer/assets/favicon.png
Normal file
BIN
.opencode/skills/markdown-novel-viewer/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Novel Theme - Main Entry Point
|
||||
* Warm, book-like reading experience with dark/light modes
|
||||
*
|
||||
* Imports all module stylesheets in correct cascade order.
|
||||
*/
|
||||
|
||||
@import url('./styles/novel-theme-variables.css');
|
||||
@import url('./styles/novel-theme-base.css');
|
||||
@import url('./styles/novel-theme-header.css');
|
||||
@import url('./styles/novel-theme-sidebar.css');
|
||||
@import url('./styles/novel-theme-content.css');
|
||||
@import url('./styles/novel-theme-components.css');
|
||||
@import url('./styles/novel-theme-mermaid.css');
|
||||
@import url('./styles/novel-theme-overlays.css');
|
||||
@import url('./styles/novel-theme-responsive.css');
|
||||
850
.opencode/skills/markdown-novel-viewer/assets/reader.js
Normal file
850
.opencode/skills/markdown-novel-viewer/assets/reader.js
Normal file
@@ -0,0 +1,850 @@
|
||||
/**
|
||||
* Reader.js - Client-side interactivity for novel viewer
|
||||
* Handles theme toggle, font size, sidebar, and keyboard navigation
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// DOM Elements
|
||||
const html = document.documentElement;
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const sidebarToggle = document.getElementById('sidebar-toggle');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const fontBtns = document.querySelectorAll('.font-btn');
|
||||
const hljsLight = document.getElementById('hljs-light');
|
||||
const hljsDark = document.getElementById('hljs-dark');
|
||||
const header = document.querySelector('.reader-header');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressFill = progressBar?.querySelector('.progress-bar-fill');
|
||||
const shortcutsToast = document.getElementById('shortcuts-toast');
|
||||
const shortcutsOverlay = document.getElementById('shortcuts-overlay');
|
||||
|
||||
// Storage keys (shared with kanban dashboard for theme persistence)
|
||||
const THEME_KEY = 'theme';
|
||||
const FONT_KEY = 'novel-viewer-font';
|
||||
const SIDEBAR_KEY = 'novel-viewer-sidebar';
|
||||
const TOAST_SHOWN_KEY = 'reader:shortcuts-toast-shown';
|
||||
|
||||
// Scroll state
|
||||
let lastScrollY = 0;
|
||||
let scrollTicking = false;
|
||||
|
||||
// Initialize theme
|
||||
function initTheme() {
|
||||
const stored = localStorage.getItem(THEME_KEY);
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const theme = stored || (prefersDark ? 'dark' : 'light');
|
||||
|
||||
setTheme(theme);
|
||||
}
|
||||
|
||||
// Set theme
|
||||
function setTheme(theme, skipMermaid = false) {
|
||||
html.dataset.theme = theme;
|
||||
localStorage.setItem(THEME_KEY, theme);
|
||||
|
||||
// Switch highlight.js theme
|
||||
if (hljsLight && hljsDark) {
|
||||
hljsLight.disabled = theme === 'dark';
|
||||
hljsDark.disabled = theme === 'light';
|
||||
}
|
||||
|
||||
// Re-render mermaid diagrams with new theme (skip on initial load)
|
||||
if (!skipMermaid && window.mermaidModule) {
|
||||
updateMermaidTheme();
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle theme
|
||||
function toggleTheme() {
|
||||
const current = html.dataset.theme || 'light';
|
||||
const next = current === 'light' ? 'dark' : 'light';
|
||||
setTheme(next);
|
||||
}
|
||||
|
||||
// Initialize font size
|
||||
function initFontSize() {
|
||||
const stored = localStorage.getItem(FONT_KEY) || 'M';
|
||||
setFontSize(stored);
|
||||
}
|
||||
|
||||
// Set font size
|
||||
function setFontSize(size) {
|
||||
html.dataset.fontSize = size;
|
||||
localStorage.setItem(FONT_KEY, size);
|
||||
|
||||
// Update button states
|
||||
fontBtns.forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.size === size);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize sidebar
|
||||
function initSidebar() {
|
||||
const stored = localStorage.getItem(SIDEBAR_KEY);
|
||||
const isMobile = window.innerWidth <= 900;
|
||||
|
||||
if (isMobile) {
|
||||
sidebar?.classList.add('hidden');
|
||||
} else if (stored === 'hidden') {
|
||||
sidebar?.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
function toggleSidebar() {
|
||||
const isHidden = sidebar?.classList.toggle('hidden');
|
||||
localStorage.setItem(SIDEBAR_KEY, isHidden ? 'hidden' : 'visible');
|
||||
}
|
||||
|
||||
// Show shortcuts toast (first visit)
|
||||
function showToast() {
|
||||
if (!shortcutsToast) return;
|
||||
|
||||
const hasShown = localStorage.getItem(TOAST_SHOWN_KEY);
|
||||
if (hasShown) return;
|
||||
|
||||
// Show toast after short delay
|
||||
setTimeout(() => {
|
||||
shortcutsToast.classList.add('show');
|
||||
|
||||
// Auto-dismiss after 5 seconds
|
||||
setTimeout(() => {
|
||||
dismissToast();
|
||||
}, 5000);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Dismiss toast
|
||||
function dismissToast() {
|
||||
if (!shortcutsToast) return;
|
||||
shortcutsToast.classList.remove('show');
|
||||
localStorage.setItem(TOAST_SHOWN_KEY, 'true');
|
||||
}
|
||||
|
||||
// Show shortcuts cheatsheet
|
||||
function showCheatsheet() {
|
||||
if (!shortcutsOverlay) return;
|
||||
shortcutsOverlay.removeAttribute('hidden');
|
||||
shortcutsOverlay.setAttribute('aria-hidden', 'false');
|
||||
// Focus trap
|
||||
const closeBtn = shortcutsOverlay.querySelector('.modal-close');
|
||||
closeBtn?.focus();
|
||||
}
|
||||
|
||||
// Hide shortcuts cheatsheet
|
||||
function hideCheatsheet() {
|
||||
if (!shortcutsOverlay) return;
|
||||
shortcutsOverlay.setAttribute('hidden', '');
|
||||
shortcutsOverlay.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
// Initialize shortcuts
|
||||
function initShortcuts() {
|
||||
// Toast dismiss button
|
||||
const dismissBtn = shortcutsToast?.querySelector('.toast-dismiss');
|
||||
dismissBtn?.addEventListener('click', dismissToast);
|
||||
|
||||
// Cheatsheet close button
|
||||
const closeBtn = shortcutsOverlay?.querySelector('.modal-close');
|
||||
closeBtn?.addEventListener('click', hideCheatsheet);
|
||||
|
||||
// Backdrop click
|
||||
const backdrop = shortcutsOverlay?.querySelector('.shortcuts-backdrop');
|
||||
backdrop?.addEventListener('click', hideCheatsheet);
|
||||
|
||||
// Show toast on first visit
|
||||
showToast();
|
||||
}
|
||||
|
||||
// Keyboard navigation
|
||||
function handleKeydown(e) {
|
||||
// Skip if in input/textarea
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close cheatsheet on Escape
|
||||
if (e.key === 'Escape' && shortcutsOverlay && !shortcutsOverlay.hasAttribute('hidden')) {
|
||||
e.preventDefault();
|
||||
hideCheatsheet();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close bottom sheet on Escape
|
||||
const bottomSheet = document.getElementById('bottom-sheet');
|
||||
if (e.key === 'Escape' && bottomSheet && bottomSheet.getAttribute('aria-hidden') === 'false') {
|
||||
e.preventDefault();
|
||||
if (window.closeBottomSheet) {
|
||||
window.closeBottomSheet();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show cheatsheet on ?
|
||||
if (e.key === '?' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
showCheatsheet();
|
||||
return;
|
||||
}
|
||||
|
||||
const navPrev = document.querySelector('.nav-prev');
|
||||
const navNext = document.querySelector('.nav-next');
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
if (navPrev) {
|
||||
e.preventDefault();
|
||||
window.location.href = navPrev.href;
|
||||
}
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (navNext) {
|
||||
e.preventDefault();
|
||||
window.location.href = navNext.href;
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
if (window.innerWidth <= 900 && sidebar && !sidebar.classList.contains('hidden')) {
|
||||
toggleSidebar();
|
||||
}
|
||||
break;
|
||||
case 't':
|
||||
case 'T':
|
||||
if (!e.ctrlKey && !e.metaKey) {
|
||||
toggleTheme();
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
case 'S':
|
||||
if (!e.ctrlKey && !e.metaKey) {
|
||||
toggleSidebar();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Smooth scroll to anchor with sidebar active state update
|
||||
function handleAnchorClick(e) {
|
||||
const anchor = e.target.closest('a');
|
||||
const href = anchor?.getAttribute('href');
|
||||
if (href?.startsWith('#')) {
|
||||
e.preventDefault();
|
||||
const targetId = href.slice(1);
|
||||
const target = document.getElementById(targetId);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
history.pushState(null, '', href);
|
||||
// Update sidebar active state
|
||||
updateSidebarActiveState(targetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update sidebar active state based on anchor
|
||||
function updateSidebarActiveState(anchorId) {
|
||||
const planNav = document.getElementById('plan-nav');
|
||||
if (!planNav) return;
|
||||
|
||||
// Remove active from all items
|
||||
planNav.querySelectorAll('.phase-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// Add active to matching item
|
||||
const matchingItem = planNav.querySelector(`[data-anchor="${anchorId}"]`);
|
||||
if (matchingItem) {
|
||||
matchingItem.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Setup Intersection Observer for section tracking
|
||||
function setupSectionObserver() {
|
||||
const planNav = document.getElementById('plan-nav');
|
||||
if (!planNav) return;
|
||||
|
||||
// Get all anchors from sidebar
|
||||
const anchors = Array.from(planNav.querySelectorAll('[data-anchor]'))
|
||||
.map(item => item.dataset.anchor);
|
||||
|
||||
if (anchors.length === 0) return;
|
||||
|
||||
// Find corresponding elements in content
|
||||
const sections = anchors
|
||||
.map(id => document.getElementById(id))
|
||||
.filter(el => el !== null);
|
||||
|
||||
if (sections.length === 0) return;
|
||||
|
||||
// Create observer
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
updateSidebarActiveState(entry.target.id);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: '-20% 0px -60% 0px', // Trigger when section is in upper portion of viewport
|
||||
threshold: 0
|
||||
});
|
||||
|
||||
// Observe all sections
|
||||
sections.forEach(section => observer.observe(section));
|
||||
}
|
||||
|
||||
// Handle hash change (browser back/forward)
|
||||
function handleHashChange() {
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
const targetId = hash.slice(1);
|
||||
const target = document.getElementById(targetId);
|
||||
if (target) {
|
||||
updateSidebarActiveState(targetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Throttle utility
|
||||
function throttle(func, wait) {
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null;
|
||||
func.apply(this, args);
|
||||
}, wait);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Update progress bar based on scroll position
|
||||
function updateProgressBar() {
|
||||
if (!progressFill) return;
|
||||
|
||||
const scrollTop = window.scrollY;
|
||||
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
||||
|
||||
progressFill.style.width = `${Math.min(progress, 100)}%`;
|
||||
}
|
||||
|
||||
// Handle scroll for progress bar and fixed header shadow
|
||||
function handleScroll() {
|
||||
if (!header) return;
|
||||
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
// Add fixed shadow when scrolled past header
|
||||
if (currentScrollY > 60) {
|
||||
header.classList.add('is-fixed');
|
||||
} else {
|
||||
header.classList.remove('is-fixed');
|
||||
}
|
||||
|
||||
lastScrollY = currentScrollY;
|
||||
|
||||
// Update progress bar using requestAnimationFrame
|
||||
if (!scrollTicking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
updateProgressBar();
|
||||
scrollTicking = false;
|
||||
});
|
||||
scrollTicking = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Mermaid diagrams
|
||||
async function initMermaid() {
|
||||
// Wait for mermaid module to load (imported in template.html)
|
||||
let attempts = 0;
|
||||
while (!window.mermaidModule && attempts < 50) {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (!window.mermaidModule) {
|
||||
console.warn('Mermaid module not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
const mermaid = window.mermaidModule;
|
||||
const isDark = html.dataset.theme === 'dark';
|
||||
|
||||
// Initialize mermaid with theme
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: isDark ? 'dark' : 'default',
|
||||
securityLevel: 'loose',
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
});
|
||||
|
||||
// Find unprocessed mermaid elements (both pre and div)
|
||||
const diagrams = document.querySelectorAll('.mermaid:not([data-processed="true"])');
|
||||
|
||||
if (diagrams.length === 0) {
|
||||
return; // Nothing to render
|
||||
}
|
||||
|
||||
// Store original source before mermaid replaces content (for theme switching)
|
||||
diagrams.forEach(el => {
|
||||
if (!el.dataset.mermaidSource) {
|
||||
el.dataset.mermaidSource = el.textContent;
|
||||
}
|
||||
});
|
||||
|
||||
// Use mermaid.run() - the preferred API for v10+
|
||||
try {
|
||||
await mermaid.run({
|
||||
nodes: diagrams,
|
||||
suppressErrors: false
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Mermaid run error:', err);
|
||||
// Show errors inline for diagrams that failed
|
||||
diagrams.forEach(el => {
|
||||
if (!el.querySelector('svg') && !el.hasAttribute('data-processed')) {
|
||||
const code = el.dataset.mermaidSource || el.textContent;
|
||||
el.innerHTML = `<div class="mermaid-error">
|
||||
<strong>Mermaid Error:</strong>
|
||||
<pre>${err.message || err}</pre>
|
||||
<details><summary>Source</summary><pre>${code}</pre></details>
|
||||
</div>`;
|
||||
el.classList.add('mermaid-error-container');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Re-render mermaid on theme change
|
||||
async function updateMermaidTheme() {
|
||||
if (!window.mermaidModule) return;
|
||||
|
||||
const isDark = html.dataset.theme === 'dark';
|
||||
|
||||
// Re-initialize with new theme
|
||||
window.mermaidModule.initialize({
|
||||
startOnLoad: false,
|
||||
theme: isDark ? 'dark' : 'default',
|
||||
securityLevel: 'loose',
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
});
|
||||
|
||||
// Restore original source and re-render
|
||||
const diagrams = document.querySelectorAll('.mermaid[data-processed="true"]');
|
||||
diagrams.forEach(el => {
|
||||
const source = el.dataset.mermaidSource;
|
||||
if (source) {
|
||||
el.textContent = source;
|
||||
el.removeAttribute('data-processed');
|
||||
}
|
||||
});
|
||||
|
||||
await initMermaid();
|
||||
// Re-init expand buttons after re-render
|
||||
setTimeout(() => initMermaidExpand(), 100);
|
||||
}
|
||||
|
||||
// Compute width and left offset for an expanded wrapper to fill .main-content
|
||||
// without using viewport units (immune to sidebar state changes)
|
||||
function applyExpandLayout(wrapper, isExpanded) {
|
||||
if (!isExpanded) {
|
||||
wrapper.style.width = '';
|
||||
wrapper.style.left = '';
|
||||
return;
|
||||
}
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
if (!mainContent) return;
|
||||
const mainRect = mainContent.getBoundingClientRect();
|
||||
const mainPadding = parseFloat(getComputedStyle(mainContent).paddingLeft) || 0;
|
||||
const availableWidth = mainRect.width - mainPadding * 2;
|
||||
const wrapperRect = wrapper.getBoundingClientRect();
|
||||
const offset = mainRect.left + mainPadding - wrapperRect.left;
|
||||
wrapper.style.width = availableWidth + 'px';
|
||||
wrapper.style.left = offset + 'px';
|
||||
}
|
||||
|
||||
// Recalculate expanded wrappers on resize or sidebar toggle
|
||||
window.addEventListener('resize', () => {
|
||||
document.querySelectorAll('.mermaid-wrapper.expanded, .code-wrapper.expanded')
|
||||
.forEach(w => applyExpandLayout(w, true));
|
||||
});
|
||||
|
||||
// Initialize Mermaid expand toggle buttons
|
||||
function initMermaidExpand() {
|
||||
// Find all rendered mermaid diagrams not already wrapped
|
||||
const diagrams = document.querySelectorAll('.mermaid[data-processed="true"]');
|
||||
|
||||
diagrams.forEach(diagram => {
|
||||
// Skip if already wrapped
|
||||
if (diagram.parentElement?.classList.contains('mermaid-wrapper')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wrapper
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'mermaid-wrapper';
|
||||
|
||||
// Create expand button
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'mermaid-expand-btn';
|
||||
btn.setAttribute('aria-label', 'Expand diagram to full width');
|
||||
btn.innerHTML = `
|
||||
<span class="icon-expand">⤢</span>
|
||||
<span class="icon-collapse">⤡</span>
|
||||
`;
|
||||
|
||||
// Toggle handler — expand to fill .main-content, re-render at new width
|
||||
btn.addEventListener('click', async () => {
|
||||
const isExpanded = wrapper.classList.toggle('expanded');
|
||||
btn.setAttribute('aria-label', isExpanded
|
||||
? 'Collapse diagram'
|
||||
: 'Expand diagram to full width'
|
||||
);
|
||||
applyExpandLayout(wrapper, isExpanded);
|
||||
|
||||
// Re-render mermaid at the new container width for crisp output
|
||||
const source = diagram.dataset.mermaidSource;
|
||||
if (source && window.mermaidModule) {
|
||||
diagram.textContent = source;
|
||||
diagram.removeAttribute('data-processed');
|
||||
try {
|
||||
await window.mermaidModule.run({ nodes: [diagram], suppressErrors: false });
|
||||
} catch (e) {
|
||||
console.error('Mermaid re-render on expand failed:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Insert wrapper before diagram
|
||||
diagram.parentNode.insertBefore(wrapper, diagram);
|
||||
|
||||
// Move diagram into wrapper
|
||||
wrapper.appendChild(diagram);
|
||||
|
||||
// Add button to wrapper
|
||||
wrapper.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize code block expand toggle buttons (for wide ASCII art)
|
||||
function initCodeExpand() {
|
||||
// Find all pre elements that are not mermaid and not already wrapped
|
||||
const codeBlocks = document.querySelectorAll('pre:not(.mermaid)');
|
||||
|
||||
codeBlocks.forEach(pre => {
|
||||
// Skip if already wrapped or inside mermaid error
|
||||
if (pre.parentElement?.classList.contains('code-wrapper') ||
|
||||
pre.parentElement?.classList.contains('mermaid-error')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add expand button if content is wider than container
|
||||
// Check if scrollWidth > clientWidth (has horizontal overflow)
|
||||
if (pre.scrollWidth <= pre.clientWidth + 10) {
|
||||
return; // No overflow, no need for expand button
|
||||
}
|
||||
|
||||
// Create wrapper
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'code-wrapper';
|
||||
|
||||
// Create expand button
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'code-expand-btn';
|
||||
btn.setAttribute('aria-label', 'Expand code block to full width');
|
||||
btn.innerHTML = `
|
||||
<span class="icon-expand">⤢</span>
|
||||
<span class="icon-collapse">⤡</span>
|
||||
`;
|
||||
|
||||
// Toggle handler — expand to fill .main-content
|
||||
btn.addEventListener('click', () => {
|
||||
const isExpanded = wrapper.classList.toggle('expanded');
|
||||
btn.setAttribute('aria-label', isExpanded
|
||||
? 'Collapse code block'
|
||||
: 'Expand code block to full width'
|
||||
);
|
||||
applyExpandLayout(wrapper, isExpanded);
|
||||
});
|
||||
|
||||
// Insert wrapper before pre
|
||||
pre.parentNode.insertBefore(wrapper, pre);
|
||||
|
||||
// Move pre into wrapper
|
||||
wrapper.appendChild(pre);
|
||||
|
||||
// Add button to wrapper
|
||||
wrapper.appendChild(btn);
|
||||
|
||||
// Auto-expand overflowing code blocks to fill available width
|
||||
// (user can collapse back via the button)
|
||||
wrapper.classList.add('expanded');
|
||||
btn.setAttribute('aria-label', 'Collapse code block');
|
||||
applyExpandLayout(wrapper, true);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
|
||||
// Initialize accordion
|
||||
function initAccordion() {
|
||||
const phaseHeaders = document.querySelectorAll('.phase-header');
|
||||
if (phaseHeaders.length === 0) return;
|
||||
|
||||
// Get plan identifier for localStorage key
|
||||
const planNav = document.getElementById('plan-nav');
|
||||
if (!planNav) return;
|
||||
const planName = planNav.querySelector('.plan-title span:last-child')?.textContent || 'unknown';
|
||||
const planId = planName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
||||
|
||||
phaseHeaders.forEach(header => {
|
||||
const phaseGroup = header.closest('.phase-group');
|
||||
if (!phaseGroup) return;
|
||||
|
||||
const phaseId = phaseGroup.dataset.phaseId;
|
||||
const storageKey = `reader:accordion:${planId}:${phaseId}`;
|
||||
|
||||
// Restore collapsed state from localStorage
|
||||
try {
|
||||
const collapsed = localStorage.getItem(storageKey);
|
||||
if (collapsed === 'true') {
|
||||
phaseGroup.classList.add('collapsed');
|
||||
}
|
||||
} catch (e) {
|
||||
// Graceful fallback if localStorage unavailable
|
||||
}
|
||||
|
||||
// Toggle handler
|
||||
const toggleAccordion = () => {
|
||||
const isCollapsed = phaseGroup.classList.toggle('collapsed');
|
||||
|
||||
// Persist state
|
||||
try {
|
||||
localStorage.setItem(storageKey, isCollapsed);
|
||||
} catch (e) {
|
||||
// Graceful fallback
|
||||
}
|
||||
};
|
||||
|
||||
// Click handler
|
||||
header.addEventListener('click', toggleAccordion);
|
||||
|
||||
// Keyboard accessibility
|
||||
header.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleAccordion();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize mobile navigation (FAB + bottom sheet)
|
||||
function initMobileNav() {
|
||||
const fabMenu = document.getElementById('fab-menu');
|
||||
const fabNext = document.querySelector('.nav-next-mobile');
|
||||
const fabPrev = document.querySelector('.nav-prev-mobile');
|
||||
const bottomSheet = document.getElementById('bottom-sheet');
|
||||
const bottomSheetContent = document.getElementById('bottom-sheet-content');
|
||||
const backdrop = bottomSheet?.querySelector('.bottom-sheet-backdrop');
|
||||
const handle = bottomSheet?.querySelector('.bottom-sheet-handle');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
|
||||
if (!fabMenu || !bottomSheet) return;
|
||||
|
||||
// Set FAB hrefs from footer navigation
|
||||
const navNext = document.querySelector('.nav-next');
|
||||
const navPrev = document.querySelector('.nav-prev');
|
||||
if (navNext && fabNext) {
|
||||
fabNext.href = navNext.href;
|
||||
}
|
||||
if (navPrev && fabPrev) {
|
||||
fabPrev.href = navPrev.href;
|
||||
}
|
||||
|
||||
// Clone sidebar content to bottom sheet
|
||||
if (sidebar && bottomSheetContent) {
|
||||
bottomSheetContent.innerHTML = sidebar.innerHTML;
|
||||
}
|
||||
|
||||
// Open bottom sheet
|
||||
function openBottomSheet() {
|
||||
bottomSheet.setAttribute('aria-hidden', 'false');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
// Close bottom sheet
|
||||
function closeBottomSheet() {
|
||||
bottomSheet.setAttribute('aria-hidden', 'true');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// FAB menu click handler
|
||||
fabMenu.addEventListener('click', openBottomSheet);
|
||||
|
||||
// Backdrop click handler
|
||||
backdrop?.addEventListener('click', closeBottomSheet);
|
||||
|
||||
// Swipe down gesture on handle
|
||||
let touchStartY = 0;
|
||||
let touchEndY = 0;
|
||||
|
||||
handle?.addEventListener('touchstart', (e) => {
|
||||
touchStartY = e.touches[0].clientY;
|
||||
}, { passive: true });
|
||||
|
||||
handle?.addEventListener('touchmove', (e) => {
|
||||
touchEndY = e.touches[0].clientY;
|
||||
}, { passive: true });
|
||||
|
||||
handle?.addEventListener('touchend', () => {
|
||||
const swipeDistance = touchEndY - touchStartY;
|
||||
// Swipe down threshold: 50px
|
||||
if (swipeDistance > 50) {
|
||||
closeBottomSheet();
|
||||
}
|
||||
touchStartY = 0;
|
||||
touchEndY = 0;
|
||||
});
|
||||
|
||||
// Close on resize to desktop
|
||||
let resizeTimeout;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
if (window.innerWidth > 768 && bottomSheet.getAttribute('aria-hidden') === 'false') {
|
||||
closeBottomSheet();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Store close function for keyboard handler
|
||||
window.closeBottomSheet = closeBottomSheet;
|
||||
}
|
||||
|
||||
// Sidebar resize functionality
|
||||
const SIDEBAR_WIDTH_KEY = 'novel-viewer-sidebar-width';
|
||||
const resizeHandle = document.getElementById('sidebar-resize');
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
|
||||
function initSidebarResize() {
|
||||
if (!resizeHandle || !sidebar) return;
|
||||
|
||||
// Restore saved width
|
||||
const savedWidth = localStorage.getItem(SIDEBAR_WIDTH_KEY);
|
||||
if (savedWidth) {
|
||||
const width = parseInt(savedWidth, 10);
|
||||
if (width >= 200 && width <= 480) {
|
||||
sidebar.style.width = width + 'px';
|
||||
if (mainContent) mainContent.style.marginLeft = width + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
let isDragging = false;
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
|
||||
resizeHandle.addEventListener('mousedown', (e) => {
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startWidth = sidebar.offsetWidth;
|
||||
resizeHandle.classList.add('dragging');
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
const delta = e.clientX - startX;
|
||||
const newWidth = Math.min(480, Math.max(200, startWidth + delta));
|
||||
sidebar.style.width = newWidth + 'px';
|
||||
if (mainContent) mainContent.style.marginLeft = newWidth + 'px';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
resizeHandle.classList.remove('dragging');
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
localStorage.setItem(SIDEBAR_WIDTH_KEY, sidebar.offsetWidth.toString());
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
initTheme();
|
||||
initFontSize();
|
||||
initSidebar();
|
||||
initSidebarResize();
|
||||
initAccordion();
|
||||
initShortcuts();
|
||||
initMobileNav();
|
||||
initMermaid().then(() => {
|
||||
// Initialize expand buttons after mermaid renders
|
||||
setTimeout(() => initMermaidExpand(), 100);
|
||||
});
|
||||
// Initialize code block expand buttons after fonts load
|
||||
// (font metrics affect scrollWidth used for overflow detection)
|
||||
if (document.fonts && document.fonts.ready) {
|
||||
document.fonts.ready.then(() => initCodeExpand());
|
||||
} else {
|
||||
// Fallback for browsers without Font Loading API
|
||||
setTimeout(() => initCodeExpand(), 300);
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
themeToggle?.addEventListener('click', toggleTheme);
|
||||
sidebarToggle?.addEventListener('click', toggleSidebar);
|
||||
|
||||
fontBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => setFontSize(btn.dataset.size));
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
document.addEventListener('click', handleAnchorClick);
|
||||
|
||||
// Handle hash change for sidebar active state
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
|
||||
// Setup section observer for auto-highlighting sidebar
|
||||
setupSectionObserver();
|
||||
|
||||
// Handle initial hash on page load
|
||||
if (window.location.hash) {
|
||||
handleHashChange();
|
||||
}
|
||||
|
||||
// Initialize scroll state and handlers
|
||||
lastScrollY = window.scrollY;
|
||||
updateProgressBar();
|
||||
window.addEventListener('scroll', throttle(handleScroll, 100), { passive: true });
|
||||
|
||||
// Handle resize
|
||||
let resizeTimeout;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
if (window.innerWidth > 900) {
|
||||
sidebar?.classList.remove('visible');
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Listen for system theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
if (!localStorage.getItem(THEME_KEY)) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Run when DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Novel Theme - Base Styles
|
||||
* Reset, html, body fundamentals
|
||||
*/
|
||||
|
||||
/* Base Reset */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 18px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html[data-font-size="S"] { font-size: 16px; }
|
||||
html[data-font-size="M"] { font-size: 18px; }
|
||||
html[data-font-size="L"] { font-size: 20px; }
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.7;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for entire page */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 5px;
|
||||
border: 2px solid var(--bg-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Firefox scrollbar */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) var(--bg-secondary);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Novel Theme - Component Styles
|
||||
* Code blocks, blockquotes, tables, images, horizontal rules
|
||||
*/
|
||||
|
||||
/* Code */
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
background: var(--code-bg);
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--code-bg);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 1.5rem 0;
|
||||
border: 1px solid var(--border-light);
|
||||
/* Use fit-content so wide ASCII art expands beyond .content container */
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
max-width: calc(100vw - var(--sidebar-width) - 6rem);
|
||||
/* Center the expanded code block relative to .content */
|
||||
position: relative;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
/* Ensure ASCII art and wide code is scrollable, not clipped */
|
||||
white-space: pre;
|
||||
/* Always show scrollbar when content overflows */
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
/* Webkit scrollbar for code blocks */
|
||||
pre::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
/* Preserve whitespace for ASCII art */
|
||||
white-space: pre;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Blockquote */
|
||||
blockquote {
|
||||
border-left: 3px solid var(--accent);
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 0 8px 8px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--bg-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Images */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 1.5rem auto;
|
||||
display: block;
|
||||
box-shadow: 0 4px 12px var(--shadow);
|
||||
}
|
||||
|
||||
/* Horizontal rule */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Code block expand toggle (similar to mermaid) */
|
||||
.code-wrapper {
|
||||
position: relative;
|
||||
margin: 1.5rem 0;
|
||||
transition: width 0.2s ease, left 0.2s ease;
|
||||
}
|
||||
|
||||
.code-wrapper pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-expand-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 10;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
transition: all 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.code-wrapper:hover .code-expand-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.code-expand-btn:hover {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Always show button when expanded */
|
||||
.code-wrapper.expanded .code-expand-btn {
|
||||
opacity: 1;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.code-wrapper.expanded .code-expand-btn:hover {
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Expanded state for code blocks — width and offset set dynamically by JS (initCodeExpand)
|
||||
to fill .main-content without affecting sibling content or using viewport units */
|
||||
.code-wrapper.expanded {
|
||||
max-width: none;
|
||||
position: relative;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.code-wrapper.expanded pre {
|
||||
max-width: none;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Icon visibility toggle for code blocks */
|
||||
.code-expand-btn .icon-expand { display: inline; }
|
||||
.code-expand-btn .icon-collapse { display: none; }
|
||||
|
||||
.code-wrapper.expanded .code-expand-btn .icon-expand { display: none; }
|
||||
.code-wrapper.expanded .code-expand-btn .icon-collapse { display: inline; }
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Novel Theme - Content Styles
|
||||
* Main content area, typography, navigation footer
|
||||
*/
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-left: var(--sidebar-width);
|
||||
padding: 2rem;
|
||||
transition: margin-left 0.15s ease;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for main content */
|
||||
.main-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.main-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.main-content::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.main-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
body:not(.has-plan) .main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
}
|
||||
|
||||
.sidebar.hidden + .main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 4rem;
|
||||
/* Allow code blocks to visually overflow beyond content width */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 700;
|
||||
margin: 2rem 0 1rem;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-top: 0;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid var(--border);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
h2::before,
|
||||
h2::after {
|
||||
content: '';
|
||||
width: 20px;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h4, h5, h6 {
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--border);
|
||||
transition: color 0.2s, text-decoration-color 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link-hover);
|
||||
text-decoration-color: var(--link-hover);
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul, ol {
|
||||
margin: 1rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
li > ul, li > ol {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Task lists (GFM) */
|
||||
ul:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
ul:has(input[type="checkbox"]) li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-top: 0.35rem;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Navigation Footer */
|
||||
.nav-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 3rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.nav-prev, .nav-next {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-prev:hover, .nav-next:hover {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 0.875rem;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Novel Theme - Header Styles
|
||||
* Header, controls, navigation, theme toggle
|
||||
*/
|
||||
|
||||
/* Header */
|
||||
.reader-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--header-height);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 1rem;
|
||||
z-index: 100;
|
||||
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.reader-header.is-hidden {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.reader-header.is-fixed {
|
||||
box-shadow: 0 2px 8px var(--shadow);
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.back-to-dashboard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.back-to-dashboard:hover {
|
||||
color: var(--accent);
|
||||
background: var(--accent-bg);
|
||||
}
|
||||
|
||||
.back-to-dashboard svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: var(--border);
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.doc-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 1rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Back button (link styled as icon-btn) */
|
||||
a.icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
a.icon-btn.back-btn {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
a.icon-btn.back-btn:hover {
|
||||
color: var(--accent);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
/* Header Navigation (prev/next in header) */
|
||||
.header-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-left: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.header-nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-nav-btn:hover {
|
||||
color: var(--accent);
|
||||
background: var(--bg-tertiary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header-nav-btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-nav-btn.prev svg {
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
|
||||
.header-nav-btn.next svg {
|
||||
margin-left: 0.125rem;
|
||||
}
|
||||
|
||||
/* Theme toggle icons */
|
||||
.sun-icon { display: block; }
|
||||
.moon-icon { display: none; }
|
||||
[data-theme="dark"] .sun-icon { display: none; }
|
||||
[data-theme="dark"] .moon-icon { display: block; }
|
||||
|
||||
/* Font controls */
|
||||
.font-controls {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.font-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.font-btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.font-btn.active {
|
||||
background: var(--bg-primary);
|
||||
color: var(--accent);
|
||||
box-shadow: 0 1px 2px var(--shadow);
|
||||
}
|
||||
|
||||
/* Progress bar */
|
||||
.progress-bar-container {
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--bg-tertiary);
|
||||
z-index: 99;
|
||||
transition: top 0.3s ease;
|
||||
}
|
||||
|
||||
.reader-header.is-hidden ~ .progress-bar-container {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent-hover));
|
||||
transition: width 0.1s ease-out;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Novel Theme - Mermaid Diagram Styles
|
||||
* Mermaid diagrams, error containers
|
||||
*/
|
||||
|
||||
/* Mermaid Diagrams */
|
||||
pre.mermaid,
|
||||
.mermaid {
|
||||
text-align: center;
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-light);
|
||||
overflow-x: auto;
|
||||
/* Reset pre defaults for mermaid */
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
pre.mermaid[data-processed="true"],
|
||||
.mermaid-rendered {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Ensure rendered SVGs scale within their container */
|
||||
.mermaid svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.mermaid-error-container {
|
||||
background: #fff5f5;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-error-container {
|
||||
background: #3a2020;
|
||||
border-color: #6a3030;
|
||||
}
|
||||
|
||||
.mermaid-error {
|
||||
text-align: left;
|
||||
color: #842029;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-error {
|
||||
color: #f8d7da;
|
||||
}
|
||||
|
||||
.mermaid-error strong {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mermaid-error pre {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
font-size: 0.8rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mermaid-error pre {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.mermaid-error details {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mermaid-error summary {
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Mermaid expand toggle */
|
||||
.mermaid-wrapper {
|
||||
position: relative;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.mermaid-expand-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 10;
|
||||
padding: 0.375rem;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1;
|
||||
transition: all 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mermaid-wrapper:hover .mermaid-expand-btn,
|
||||
.mermaid-wrapper:focus-within .mermaid-expand-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Always show button when expanded so user can collapse */
|
||||
.mermaid-wrapper.expanded .mermaid-expand-btn {
|
||||
opacity: 1;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.mermaid-expand-btn:hover {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.mermaid-wrapper.expanded .mermaid-expand-btn:hover {
|
||||
background: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Expanded state — width and offset set dynamically by JS (initMermaidExpand)
|
||||
to fill .main-content without affecting sibling content or using viewport units */
|
||||
.mermaid-wrapper.expanded {
|
||||
max-width: none;
|
||||
position: relative;
|
||||
padding: 1rem 2rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mermaid-wrapper.expanded .mermaid {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/* Override Mermaid's inline max-width so SVG fills expanded container */
|
||||
.mermaid-wrapper.expanded .mermaid svg {
|
||||
max-width: 100% !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Icon visibility toggle */
|
||||
.mermaid-expand-btn .icon-expand { display: inline; }
|
||||
.mermaid-expand-btn .icon-collapse { display: none; }
|
||||
|
||||
.mermaid-wrapper.expanded .mermaid-expand-btn .icon-expand { display: none; }
|
||||
.mermaid-wrapper.expanded .mermaid-expand-btn .icon-collapse { display: inline; }
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Novel Theme - Overlays Module
|
||||
* Toast notifications and keyboard shortcuts cheatsheet
|
||||
*/
|
||||
|
||||
/* ===========================
|
||||
KEYBOARD SHORTCUTS TOAST
|
||||
=========================== */
|
||||
|
||||
.shortcuts-toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
z-index: 1000;
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-primary);
|
||||
opacity: 0;
|
||||
transform: translateY(1rem);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.shortcuts-toast.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.shortcuts-toast kbd {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0.2rem 0.5rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.toast-dismiss {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.toast-dismiss:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
KEYBOARD SHORTCUTS MODAL
|
||||
=========================== */
|
||||
|
||||
.shortcuts-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.shortcuts-overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.shortcuts-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shortcuts-modal {
|
||||
position: relative;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.shortcuts-modal header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.shortcuts-modal h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.75rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.shortcuts-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.shortcuts-table tbody tr {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.shortcuts-table tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.shortcuts-table td {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.shortcuts-table td:first-child {
|
||||
width: 100px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.shortcuts-table td:last-child {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.shortcuts-table kbd {
|
||||
display: inline-block;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
min-width: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
RESPONSIVE
|
||||
=========================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.shortcuts-toast {
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
left: 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.shortcuts-modal {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.shortcuts-modal header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.shortcuts-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.shortcuts-table td:first-child {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Novel Theme - Responsive Styles
|
||||
* Media queries for responsive design, print styles
|
||||
*/
|
||||
|
||||
/* Responsive - Tablet */
|
||||
@media (max-width: 900px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
box-shadow: 2px 0 8px var(--shadow);
|
||||
}
|
||||
|
||||
.sidebar.visible {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive - Mobile */
|
||||
@media (max-width: 600px) {
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.reader-header {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.font-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-footer {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-prev, .nav-next {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Hide header nav text on small screens */
|
||||
.header-nav-btn span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-nav-btn {
|
||||
padding: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile FAB (Floating Action Button) Group */
|
||||
.fab-group {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
display: none; /* Hidden on desktop */
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.fab-group {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.fab {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
min-width: 48px;
|
||||
min-height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px var(--shadow);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.fab:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 16px var(--shadow);
|
||||
}
|
||||
|
||||
.fab:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.fab svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* FAB variants */
|
||||
.fab-menu {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.fab-prev,
|
||||
.fab-next {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.fab-prev:hover,
|
||||
.fab-next:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
/* Bottom Sheet */
|
||||
.bottom-sheet {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bottom-sheet[aria-hidden="false"] {
|
||||
display: block;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bottom-sheet-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.bottom-sheet[aria-hidden="false"] .bottom-sheet-backdrop {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bottom-sheet-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 85vh;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 1rem 1rem 0 0;
|
||||
box-shadow: 0 -4px 16px var(--shadow);
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bottom-sheet[aria-hidden="false"] .bottom-sheet-container {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bottom-sheet-backdrop,
|
||||
.bottom-sheet-container,
|
||||
.fab {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-sheet-handle {
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: grab;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bottom-sheet-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.handle-bar {
|
||||
width: 3rem;
|
||||
height: 0.25rem;
|
||||
background: var(--text-muted);
|
||||
border-radius: 0.125rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.bottom-sheet-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0 1rem 1.5rem 1rem;
|
||||
flex: 1;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Clone sidebar styles for bottom sheet */
|
||||
.bottom-sheet-content .toc-section {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.bottom-sheet-content .toc-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.bottom-sheet-content .toc {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bottom-sheet-content .toc a {
|
||||
display: block;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
border-radius: 0.375rem;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.bottom-sheet-content .toc a:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Hide desktop sidebar on mobile when bottom sheet is active */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.reader-header,
|
||||
.sidebar,
|
||||
.nav-footer,
|
||||
.font-controls,
|
||||
#theme-toggle,
|
||||
#sidebar-toggle,
|
||||
.fab-group,
|
||||
.bottom-sheet {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* Novel Theme - Sidebar Styles
|
||||
* Layout, sidebar, plan navigation, TOC
|
||||
*/
|
||||
|
||||
/* Layout */
|
||||
.layout {
|
||||
display: flex;
|
||||
margin-top: var(--header-height);
|
||||
min-height: calc(100vh - var(--header-height));
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
min-width: 200px;
|
||||
max-width: 480px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: translateX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
.sidebar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Resize handle */
|
||||
.sidebar-resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -3px;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-resize-handle:hover,
|
||||
.sidebar-resize-handle.dragging {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.sidebar.hidden {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
/* Plan Navigation */
|
||||
.plan-nav {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.plan-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.plan-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.phase-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.phase-item {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Inline section items (anchors within same doc) - indented with visual distinction */
|
||||
.phase-item.inline-section {
|
||||
margin-left: 0.75rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.phase-item.inline-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -0.5rem;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: var(--border);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.phase-item a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
/* Inline sections have subtler styling */
|
||||
.phase-item.inline-section a {
|
||||
font-size: 0.8125rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.phase-item.inline-section a:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.phase-item a:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.phase-item.active a {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.phase-item.inline-section.active a {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Type indicator icon */
|
||||
.phase-type-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.phase-item.active .phase-type-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-dot.pending { background: #d4a574; }
|
||||
.status-dot.in-progress { background: #4a90d9; }
|
||||
.status-dot.completed, .status-dot.done { background: #5cb85c; }
|
||||
.status-dot.overview { background: #8b4513; }
|
||||
|
||||
/* Unavailable/Planned phases */
|
||||
.phase-item.unavailable {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.phase-item.unavailable .phase-link-disabled {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
border-radius: 6px;
|
||||
cursor: not-allowed;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px dashed var(--border);
|
||||
}
|
||||
|
||||
.phase-item.unavailable .status-dot {
|
||||
background: var(--text-muted);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.unavailable-badge,
|
||||
.nav-badge {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
background: var(--text-muted);
|
||||
color: var(--bg-primary);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Nav footer unavailable state */
|
||||
.nav-unavailable {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
border-style: dashed !important;
|
||||
}
|
||||
|
||||
.nav-unavailable .nav-badge {
|
||||
font-size: 0.6rem;
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
/* Phase accordion */
|
||||
.phase-group {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.phase-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
text-align: left;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.phase-header:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.phase-chevron {
|
||||
font-size: 0.625rem;
|
||||
color: var(--text-muted);
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.phase-group.collapsed .phase-chevron {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.phase-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Progress badges */
|
||||
.phase-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge-done {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .badge-done {
|
||||
background: #1e3a2f;
|
||||
color: #75d49b;
|
||||
}
|
||||
|
||||
.badge-progress {
|
||||
background: #cce5ff;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .badge-progress {
|
||||
background: #1a3a5c;
|
||||
color: #6db3f2;
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Phase items container — visible by default, collapsed state hides */
|
||||
.phase-items {
|
||||
list-style: none;
|
||||
padding-left: 1.25rem;
|
||||
margin-top: 0.25rem;
|
||||
transition: max-height 0.3s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.phase-group.collapsed .phase-items {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* TOC */
|
||||
.toc-section {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.toc-title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.toc-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.toc-list li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.toc-list a {
|
||||
display: block;
|
||||
padding: 0.25rem 0;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.toc-list a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Novel Theme - CSS Custom Properties
|
||||
* Light and dark theme variables
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Light Theme (Default) */
|
||||
--bg-primary: #faf8f3;
|
||||
--bg-secondary: #f5f2eb;
|
||||
--bg-tertiary: #ebe7de;
|
||||
--text-heading: #3a3a3a;
|
||||
--text-primary: #5a5a5a;
|
||||
--text-secondary: #6a6a6a;
|
||||
--text-muted: #8c8c8c;
|
||||
--accent: #8b4513;
|
||||
--accent-hover: #6d360f;
|
||||
--border: #e8e4db;
|
||||
--border-light: #f0ece3;
|
||||
--shadow: rgba(0, 0, 0, 0.08);
|
||||
--code-bg: #f8f5ef;
|
||||
--link: #5c4033;
|
||||
--link-hover: #8b4513;
|
||||
|
||||
/* Fonts */
|
||||
--font-heading: 'Libre Baskerville', Georgia, serif;
|
||||
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
/* Spacing */
|
||||
--content-width: 720px;
|
||||
--sidebar-width: 280px;
|
||||
--header-height: 56px;
|
||||
}
|
||||
|
||||
/* Content width responsive to font size */
|
||||
html[data-font-size="S"] { --content-width: 640px; }
|
||||
html[data-font-size="M"] { --content-width: 720px; }
|
||||
html[data-font-size="L"] { --content-width: 800px; }
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #252525;
|
||||
--bg-tertiary: #303030;
|
||||
--text-heading: #e0dcd3;
|
||||
--text-primary: #b0aca3;
|
||||
--text-secondary: #9a9a9a;
|
||||
--text-muted: #707070;
|
||||
--accent: #d4a574;
|
||||
--accent-hover: #e0b98a;
|
||||
--border: #3a3a3a;
|
||||
--border-light: #2a2a2a;
|
||||
--shadow: rgba(0, 0, 0, 0.3);
|
||||
--code-bg: #2a2a2a;
|
||||
--link: #d4a574;
|
||||
--link-hover: #e8c9a0;
|
||||
}
|
||||
149
.opencode/skills/markdown-novel-viewer/assets/template.html
Normal file
149
.opencode/skills/markdown-novel-viewer/assets/template.html
Normal file
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{title}} - Novel Viewer</title>
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
||||
<!-- Apply stored preferences BEFORE CSS loads to prevent FOUC -->
|
||||
<script>
|
||||
(function(){
|
||||
var h=document.documentElement;
|
||||
var t=localStorage.getItem('theme');
|
||||
var f=localStorage.getItem('novel-viewer-font');
|
||||
if(t)h.dataset.theme=t;
|
||||
else if(window.matchMedia('(prefers-color-scheme:dark)').matches)h.dataset.theme='dark';
|
||||
if(f)h.dataset.fontSize=f;
|
||||
})();
|
||||
</script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/assets/novel-theme.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" id="hljs-light">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" id="hljs-dark" disabled>
|
||||
<!-- Mermaid.js for diagram rendering -->
|
||||
<script type="module">
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||
window.mermaidModule = mermaid;
|
||||
</script>
|
||||
</head>
|
||||
<body class="{{has-plan}}">
|
||||
<header class="reader-header">
|
||||
<div class="header-left">
|
||||
{{back-button}}
|
||||
<button id="sidebar-toggle" class="icon-btn" aria-label="Toggle sidebar" title="Toggle sidebar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h18M3 6h18M3 18h18"/>
|
||||
</svg>
|
||||
</button>
|
||||
{{header-nav}}
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<span class="doc-title">{{title}}</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="font-controls">
|
||||
<button class="font-btn" data-size="S" title="Small font">S</button>
|
||||
<button class="font-btn" data-size="M" title="Medium font">M</button>
|
||||
<button class="font-btn" data-size="L" title="Large font">L</button>
|
||||
</div>
|
||||
<button id="theme-toggle" class="icon-btn" aria-label="Toggle theme" title="Toggle theme">
|
||||
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
<svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="progress-bar-container" id="progress-bar">
|
||||
<div class="progress-bar-fill"></div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-resize-handle" id="sidebar-resize"></div>
|
||||
{{nav-sidebar}}
|
||||
<div class="toc-section">
|
||||
<div class="toc-title">Contents</div>
|
||||
{{toc}}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<article class="content">
|
||||
{{content}}
|
||||
</article>
|
||||
{{nav-footer}}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Keyboard shortcuts toast -->
|
||||
<div class="shortcuts-toast" id="shortcuts-toast" role="status" aria-live="polite">
|
||||
<span>Press <kbd>?</kbd> for keyboard shortcuts</span>
|
||||
<button class="toast-dismiss" aria-label="Dismiss">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Keyboard shortcuts cheatsheet -->
|
||||
<div class="shortcuts-overlay" id="shortcuts-overlay" role="dialog" aria-modal="true" aria-labelledby="shortcuts-title" hidden>
|
||||
<div class="shortcuts-backdrop"></div>
|
||||
<div class="shortcuts-modal">
|
||||
<header>
|
||||
<h2 id="shortcuts-title">Keyboard Shortcuts</h2>
|
||||
<button class="modal-close" aria-label="Close">×</button>
|
||||
</header>
|
||||
<table class="shortcuts-table">
|
||||
<tbody>
|
||||
<tr><td><kbd>T</kbd></td><td>Toggle theme (light/dark)</td></tr>
|
||||
<tr><td><kbd>S</kbd></td><td>Toggle sidebar</td></tr>
|
||||
<tr><td><kbd>←</kbd></td><td>Previous page</td></tr>
|
||||
<tr><td><kbd>→</kbd></td><td>Next page</td></tr>
|
||||
<tr><td><kbd>Esc</kbd></td><td>Close sidebar / modal</td></tr>
|
||||
<tr><td><kbd>?</kbd></td><td>Show this help</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile FAB group -->
|
||||
<div class="fab-group" id="fab-group" aria-label="Quick navigation">
|
||||
<button class="fab fab-menu" id="fab-menu" aria-label="Open navigation menu">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h18M3 6h18M3 18h18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<a class="fab fab-next nav-next-mobile" aria-label="Next page">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 18l6-6-6-6"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="fab fab-prev nav-prev-mobile" aria-label="Previous page">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M15 18l-6-6 6-6"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile bottom sheet -->
|
||||
<div class="bottom-sheet" id="bottom-sheet" aria-hidden="true">
|
||||
<div class="bottom-sheet-backdrop"></div>
|
||||
<div class="bottom-sheet-container">
|
||||
<div class="bottom-sheet-handle" aria-label="Drag to close">
|
||||
<span class="handle-bar"></span>
|
||||
</div>
|
||||
<div class="bottom-sheet-content" id="bottom-sheet-content">
|
||||
<!-- Sidebar content cloned here via JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__frontmatter = {{frontmatter}};
|
||||
</script>
|
||||
<script src="/assets/reader.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user