UNPKG

@razen-core/zenweb

Version:

A minimalist TypeScript framework for building reactive web applications with no virtual DOM

250 lines 7.36 kB
/** * Routing Helper Functions * UI components and utilities for routing */ import { createElement } from '../dom.js'; /** * Link component - creates a router-aware link */ export function Link(props, children) { const link = document.createElement('a'); link.href = props.to; if (props.class) { link.className = props.class; } if (props.style) { Object.assign(link.style, props.style); } // Add children const childArray = Array.isArray(children) ? children : [children]; childArray.forEach(child => { if (typeof child === 'string') { link.appendChild(document.createTextNode(child)); } else { link.appendChild(child); } }); // Handle active class if (props.activeClass) { const updateActiveClass = () => { const currentPath = window.location.pathname; const isActive = props.exact ? currentPath === props.to : currentPath.startsWith(props.to); if (isActive) { link.classList.add(props.activeClass); } else { link.classList.remove(props.activeClass); } }; updateActiveClass(); window.addEventListener('popstate', updateActiveClass); } return link; } /** * NavLink component - Link with automatic active styling */ export function NavLink(props, children) { return Link({ ...props, activeClass: props.activeClass || 'active' }, children); } /** * Router outlet component - renders matched route */ export function RouterOutlet(router) { const outlet = createElement('div', { class: 'router-outlet' }); router.mount(outlet); return outlet; } /** * Route guard component - conditionally render based on route */ export function RouteGuard(condition, children, fallback) { const container = createElement('div', { class: 'route-guard' }); // This would need to be integrated with the router to work properly // For now, return the children container.appendChild(children); return container; } /** * Breadcrumb component */ export function Breadcrumb(props = {}) { const nav = createElement('nav', { class: props.class || 'breadcrumb', style: props.style }); const updateBreadcrumb = () => { nav.innerHTML = ''; const paths = window.location.pathname.split('/').filter(Boolean); // Home link const homeLink = Link({ to: '/', class: 'breadcrumb-item' }, 'Home'); nav.appendChild(homeLink); // Path segments let currentPath = ''; paths.forEach((segment, index) => { currentPath += '/' + segment; if (props.separator) { const separator = document.createTextNode(` ${props.separator} `); nav.appendChild(separator); } const isLast = index === paths.length - 1; if (isLast) { const span = createElement('span', { class: 'breadcrumb-item active' }); span.textContent = segment; nav.appendChild(span); } else { const link = Link({ to: currentPath, class: 'breadcrumb-item' }, segment); nav.appendChild(link); } }); }; updateBreadcrumb(); window.addEventListener('popstate', updateBreadcrumb); return nav; } /** * Back button component */ export function BackButton(props = {}) { const button = createElement('button', { class: props.class || 'back-button', style: props.style }); button.textContent = props.text || '← Back'; button.onclick = () => window.history.back(); return button; } /** * Route params display (for debugging) */ export function RouteParamsDebug(router) { const container = createElement('div', { class: 'route-params-debug', style: { padding: '1rem', background: '#f0f0f0', borderRadius: '4px', fontFamily: 'monospace', fontSize: '0.875rem' } }); const update = () => { const route = router.getCurrentRoute(); if (route) { container.innerHTML = ` <strong>Route Debug:</strong><br> Path: ${route.path}<br> Params: ${JSON.stringify(route.params)}<br> Query: ${JSON.stringify(route.query)}<br> Hash: ${route.hash} `; } }; update(); return container; } /** * Loading component for lazy routes */ export function RouteLoading(props = {}) { const container = createElement('div', { class: props.class || 'route-loading', style: { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '2rem', ...props.style } }); const spinner = createElement('div', { class: 'spinner', style: { width: '40px', height: '40px', border: '4px solid #f3f3f3', borderTop: '4px solid #3498db', borderRadius: '50%', animation: 'spin 1s linear infinite' } }); container.appendChild(spinner); if (props.text) { const text = createElement('span', { style: { marginLeft: '1rem' } }); text.textContent = props.text; container.appendChild(text); } // Add keyframes for spinner animation if (!document.querySelector('#route-loading-styles')) { const style = document.createElement('style'); style.id = 'route-loading-styles'; style.textContent = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); } return container; } /** * 404 Not Found component */ export function NotFound(props = {}) { const container = createElement('div', { class: props.class || 'not-found', style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '4rem 2rem', textAlign: 'center', ...props.style } }); const title = createElement('h1', { style: { fontSize: '4rem', margin: '0 0 1rem 0', color: '#333' } }); title.textContent = props.title || '404'; const message = createElement('p', { style: { fontSize: '1.25rem', margin: '0 0 2rem 0', color: '#666' } }); message.textContent = props.message || 'Page not found'; container.appendChild(title); container.appendChild(message); if (props.homeLink !== false) { const homeLink = Link({ to: '/', style: { padding: '0.75rem 1.5rem', background: '#3498db', color: 'white', textDecoration: 'none', borderRadius: '4px', fontSize: '1rem' } }, 'Go Home'); container.appendChild(homeLink); } return container; } //# sourceMappingURL=routing.js.map