lightview
Version:
A reactive UI library with features of Bau, Juris, and HTMX plus safe LLM UI generation
358 lines (310 loc) • 16.4 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lightview - Why be heavy when you can be light?</title>
<meta name="description"
content="Lightview is a lightweight reactive UI library. All the power. None of the weight. Heavy, complex frameworks? Lighten up.">
<link rel="icon" type="image/svg+xml" href="/docs/assets/images/logo-favicon.svg">
<!-- Site Styles -->
<link rel="stylesheet" href="/docs/assets/styles/site.css">
<link rel="stylesheet" href="/docs/assets/styles/themes.css">
</head>
<body>
<!-- Main App Shell -->
<div id="app">
<!-- Navigation -->
<nav id="main-nav" class="site-nav">
<div class="nav-brand">
<a href="/docs/" class="logo-link">
<img src="/docs/assets/images/logo-static.svg" alt="Lightview" class="logo">
<span class="logo-text">Lightview</span>
</a>
</div>
<button class="nav-toggle" aria-label="Toggle navigation" onclick="toggleMobileNav()">
<span class="nav-toggle-icon"></span>
</button>
<div class="nav-links">
<a href="/docs/" class="nav-link">Home</a>
<a href="/docs/getting-started/" class="nav-link">Get Started</a>
<a href="/docs/hypermedia/" class="nav-link">Hypermedia</a>
<a href="/docs/api/" class="nav-link">API</a>
<a href="/docs/styles/" class="nav-link">Styles</a>
<a href="/docs/components/" class="nav-link">Components</a>
<a href="/docs/router/" class="nav-link">Router</a>
<a href="/docs/cdom.html" class="nav-link">cDOM</a>
<!--a href="/docs/playground/" class="nav-link">Playground</a>-->
<a href="https://github.com/anywhichway/lightview" class="nav-link nav-link-external" target="_blank"
rel="noopener">
GitHub
<svg class="external-icon" viewBox="0 0 24 24" width="14" height="14" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" />
</svg>
</a>
</div>
<!-- Moved to components gallery per user request -->
<div class="nav-actions">
</div>
</nav>
<!-- Main Content Area -->
<main id="content" class="site-content">
<!-- Content loaded here via router -->
<div class="loading-spinner">
<div class="spinner"></div>
<p>Loading...</p>
</div>
</main>
<!-- Footer -->
<footer class="site-footer">
<div class="footer-content">
<div class="footer-brand">
<img src="/docs/assets/images/logo-static.svg" alt="Lightview" class="footer-logo">
<p class="footer-tagline">The view is beautiful from here.</p>
</div>
<div class="footer-links">
<div class="footer-column">
<h4>Docs</h4>
<a href="/docs/getting-started/">Getting Started</a>
<a href="/docs/hypermedia/">Hypermedia</a>
<a href="/docs/api/">API Reference</a>
<a href="/docs/styles/">Styles</a>
<a href="/docs/components">Components</a>
<a href="/docs/router/">Router</a>
<a href="/docs/cdom.html">cDOM</a>
</div>
<div class="footer-column">
<h4>Resources</h4>
<a href="https://github.com/anywhichway/lightview" target="_blank" rel="noopener">GitHub</a>
</div>
<div class="footer-column">
<h4>More</h4>
<a href="/docs/about/">About</a>
<a href="https://github.com/anywhichway/lightview/issues" target="_blank" rel="noopener">Report
Issue</a>
</div>
</div>
</div>
<div class="footer-bottom">
<p>© 2025 - 2026 Simon Y. Blackwell, AnyWhichWay, LLC. MIT License.</p>
</div>
</footer>
</div>
<!-- Lightview Core -->
<script src="/lightview.js"></script>
<script src="/lightview-x.js"></script>
<script src="/lightview-router.js"></script>
<script type="module" src="/components/navigation/breadcrumbs.js"></script>
<script src="/docs/assets/js/examplify.js"></script>
<!--script>const { $ } = Lightview;</script-->
<!-- Router -->
<script type="module">
import { localeHandler } from '/middleware/locale.js';
import { markdownHandler } from '/middleware/markdown.js';
import { notFound } from '/middleware/notFound.js';
// Mobile Navigation - Click-Outside to Dismiss
const mobileNav = (() => {
const getNav = () => document.querySelector('.nav-links');
const getToggle = () => document.querySelector('.nav-toggle');
let isOpening = false;
const close = () => {
const nav = getNav();
if (nav) nav.classList.remove('open');
};
const open = () => {
const nav = getNav();
if (nav) {
nav.classList.add('open');
isOpening = true;
// Clear flag after opening to allow outside clicks to close
setTimeout(() => { isOpening = false; }, 100);
}
};
const toggle = () => {
const nav = getNav();
if (nav && nav.classList.contains('open')) {
close();
} else {
open();
}
};
const init = () => {
const nav = getNav();
// Close when clicking links inside nav
if (nav) {
nav.addEventListener('click', (e) => {
if (e.target.closest('a')) close();
});
}
// Close when clicking outside nav or toggle button
document.addEventListener('mousedown', (event) => {
const nav = getNav();
const toggle = getToggle();
if (!nav || !nav.classList.contains('open')) return;
if (isOpening) return;
const clickedNav = nav.contains(event.target);
const clickedToggle = toggle && toggle.contains(event.target);
if (!clickedNav && !clickedToggle) close();
});
};
return { toggle, close, init };
})();
// Expose globally
globalThis.toggleMobileNav = mobileNav.toggle;
globalThis.closeMobileNav = mobileNav.close;
// Initialize
mobileNav.init();
// SCROLL RESTORATION: Capture clicked link IDs
document.addEventListener('click', (e) => {
const link = e.target.closest('a');
if (link && link.id) {
const currentState = history.state || {};
const updatedState = { ...currentState, lastClickedId: link.id };
history.replaceState(updatedState, '', location.href);
}
}, {
capture: true
});
(async function () {
// Initialize components with shadow DOM enabled by default
//await LightviewX.initComponents({ shadowDefault: true });
await LightviewX.registerStyleSheet('/docs/components/index.css');
await LightviewX.registerThemeSheet('/docs/assets/styles/themes.css');
const contentEl = document.getElementById('content');
const navLinks = document.querySelectorAll('.nav-link');
// Theme Selector Initializer
globalThis.initThemeSelector = (container) => {
if (!container) return;
if (container.querySelector('select')) return; // Already initialized
const themes = ['light', 'dark', 'cupcake', 'bumblebee', 'emerald', 'corporate', 'synthwave', 'retro', 'cyberpunk', 'valentine', 'halloween', 'garden', 'forest', 'aqua', 'lofi', 'pastel', 'fantasy', 'wireframe', 'black', 'luxury', 'dracula', 'cmyk', 'autumn', 'business', 'acid', 'lemonade', 'night', 'coffee', 'winter', 'dim', 'nord', 'sunset'];
const currentTheme = document.documentElement.getAttribute('data-theme') || localStorage.getItem('lightview-theme') || 'light';
const select = document.createElement('select');
Object.assign(select.style, {
padding: '0.25rem',
borderRadius: '0.25rem',
border: '1px solid #ccc',
background: 'var(--b1, #fff)',
color: 'var(--bc, #000)',
fontSize: '0.875rem',
cursor: 'pointer'
});
themes.forEach(t => {
const option = document.createElement('option');
option.value = t;
option.textContent = t.charAt(0).toUpperCase() + t.slice(1);
if (t === currentTheme) option.selected = true;
select.appendChild(option);
});
select.addEventListener('change', (e) => {
LightviewX.setTheme(e.target.value);
});
container.appendChild(select);
};
// Setup Theme Selector if container exists (e.g. if we add it back to header later)
initThemeSelector(document.getElementById('theme-toggle-container'));
// Helper: Update active nav link
function updateActiveNav(path) {
navLinks.forEach(link => {
const href = link.getAttribute('href');
const linkPath = href.startsWith('https://') || href.startsWith('http://') ? href : (href.startsWith('#') ? href.slice(1) : href);
// Normalize paths for comparison
const normalizedPath = path === '/' ? '/docs/' : path;
const normalizedLinkPath = linkPath === '/' ? '/docs/' : linkPath;
// For home page (/docs/), only highlight the "Home" link
if (normalizedPath === '/docs/' || normalizedPath === '/docs') {
if (normalizedLinkPath === '/docs/' || normalizedLinkPath === '/docs') {
link.classList.add('active');
} else {
link.classList.remove('active');
}
} else {
// For other pages, check for exact match or path prefix
// Use exact match first, then check if it's a valid sub-path
if (normalizedLinkPath === normalizedPath) {
link.classList.add('active');
} else if (normalizedPath.startsWith(normalizedLinkPath + '/') ||
normalizedPath.startsWith(normalizedLinkPath + '-')) {
// Match sub-paths like /docs/api matches /docs/api/elements
link.classList.add('active');
} else {
link.classList.remove('active');
}
}
});
}
// Initialize Router with contentEl for automatic rendering
const appRouter = LightviewRouter.router({
contentEl: contentEl, // Routes will render here automatically
// On Start (Loading UI)
onStart: (path) => {
// Close mobile nav if open
if (globalThis.closeMobileNav) globalThis.closeMobileNav();
contentEl.innerHTML = `
<div class="loading-spinner">
<div class="spinner"></div>
</div>
`;
updateActiveNav(path);
},
// On Response - called AFTER auto-render for post-render logic
onResponse: (response, path) => {
if (globalThis.closeMobileNav) globalThis.closeMobileNav(); // Ensure menu closes on navigation
// Inject Theme Selector into Components Gallery if present
const galleryHeader = contentEl.querySelector('.gallery-header');
if (galleryHeader && !galleryHeader.querySelector('#theme-toggle-container')) {
const container = document.createElement('div');
container.id = 'theme-toggle-container';
container.style.marginLeft = 'auto';
galleryHeader.appendChild(container);
if (globalThis.initThemeSelector) {
globalThis.initThemeSelector(container);
}
}
// Smart Scroll Restoration
const restoreId = history.state?.lastClickedId;
const targetEl = restoreId ? document.getElementById(restoreId) : null;
// Check the destination PATH for a hash, not window.location (which might be stale)
const hasHash = path.includes('#');
if (targetEl) {
requestAnimationFrame(() => {
targetEl.scrollIntoView({
block: 'center',
behavior: 'smooth'
});
});
} else if (!hasHash) {
globalThis.scrollTo(0, 0);
}
}
});
// Static Routes - automatic fetch when contentEl is set
appRouter.use('/', '/docs/index.html');
appRouter.use('/docs', '/docs/index.html');
appRouter.use('/docs/index.html');
appRouter.use('/docs/getting-started', '/docs/getting-started/index.html');
appRouter.use('/docs/about/', '/docs/about.html');
appRouter.use('/docs/styles/', '/docs/styles/index.html');
appRouter.use('/docs/api/', '/docs/api/index.html');
appRouter.use('/docs/components/', '/docs/components/index.html');
appRouter.use('/docs/examples/', '/docs/examples/index.html');
appRouter.use('/docs/router/', '/docs/router.html');
appRouter.use('/docs/hypermedia/', '/docs/hypermedia/index.html');
appRouter.use('/docs/hypermedia/index.html');
appRouter.use('/docs/cdom.html');
// Wildcard Routes - path replacement with automatic fetch
appRouter.use('/docs/api/*');
appRouter.use('/docs/components/*');
appRouter.use('/docs/examples/*');
// Direct HTML files
appRouter.use('/docs/*.html');
// Fallback Routes (Try appending .html)
appRouter.use('/docs/*', '/docs/*.html');
// 404 Handler - uses contentEl from context
appRouter.use(notFound());
// Start Router
appRouter.start();
})();
</script>
</body>
</html>