Spaces:
Sleeping
Sleeping
| """ | |
| Layout Components for Rubric AI | |
| Contains: sidebar, base_layout, dark_mode_toggle, breadcrumbs, page_header | |
| """ | |
| from contextlib import contextmanager | |
| from pathlib import Path | |
| from nicegui import ui, app | |
| # Get the base directory for static files (project root) | |
| BASE_DIR = Path(__file__).parent.parent.parent | |
| def add_head_resources(): | |
| """Add required CSS and fonts to the page head.""" | |
| # CRITICAL: Apply sidebar state BEFORE anything else loads | |
| # This blocking script runs synchronously before first paint | |
| ui.add_head_html(''' | |
| <script> | |
| (function() { | |
| if (localStorage.getItem('sidebarCollapsed') === 'true') { | |
| document.documentElement.classList.add('sidebar-collapsed'); | |
| } | |
| })(); | |
| </script> | |
| ''') | |
| # Google Fonts - Inter | |
| ui.add_head_html('<link rel="preconnect" href="https://fonts.googleapis.com">') | |
| ui.add_head_html('<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>') | |
| ui.add_head_html('<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;900&display=swap" rel="stylesheet">') | |
| # Material Symbols | |
| ui.add_head_html('<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" rel="stylesheet">') | |
| # SortableJS for drag and drop | |
| ui.add_head_html('<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>') | |
| # Custom CSS - use absolute path | |
| css_path = BASE_DIR / 'static' / 'styles.css' | |
| ui.add_css(css_path.read_text()) | |
| # Update sidebar icon after DOM loads | |
| ui.add_head_html(''' | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const icon = document.querySelector('.sidebar-collapse-icon'); | |
| if (icon && document.documentElement.classList.contains('sidebar-collapsed')) { | |
| icon.textContent = 'chevron_right'; | |
| } | |
| }); | |
| </script> | |
| ''') | |
| def material_icon(name: str, classes: str = '') -> ui.html: | |
| """Helper to create a Material Symbols icon.""" | |
| return ui.html(f'<span class="material-symbols-outlined {classes}">{name}</span>', sanitize=False) | |
| def dark_mode_toggle(): | |
| """Create a dark mode toggle button.""" | |
| async def toggle_dark_mode(): | |
| # Toggle the dark class on html element | |
| await ui.run_javascript(''' | |
| document.documentElement.classList.toggle("dark"); | |
| return document.documentElement.classList.contains("dark"); | |
| ''') | |
| with ui.element('div').classes('dark-mode-toggle').on('click', toggle_dark_mode): | |
| material_icon('dark_mode') | |
| ui.label('Theme').classes('text-sm font-medium nav-item-label') | |
| def sidebar(active_page: str = 'rubrics', on_logout=None): | |
| """ | |
| Create the sidebar navigation component. | |
| Args: | |
| active_page: Current active page ('dashboard', 'rubrics', 'templates', 'analytics', 'history') | |
| on_logout: Callback function for logout action | |
| """ | |
| nav_items = [ | |
| ('dashboard', 'dashboard', 'Dashboard', '/'), | |
| ('rubrics', 'edit_document', 'Rubrics', '/'), | |
| ('reference', 'grid_view', 'Reference Docs', '/reference'), | |
| ('analytics', 'bar_chart', 'Analytics', '#'), | |
| ('history', 'history', 'History', '#'), | |
| ] | |
| bottom_items = [ | |
| ('settings', 'settings', 'Settings', '#'), | |
| ('help', 'help', 'Help', '#'), | |
| ] | |
| with ui.element('aside').classes('sidebar'): | |
| # Top section | |
| with ui.element('div').classes('flex flex-col gap-4'): | |
| # Logo/Header with collapse button | |
| with ui.element('div').classes('sidebar-header'): | |
| ui.html('<h1 class="sidebar-title">Rubric AI</h1>', sanitize=False) | |
| with ui.element('button').classes('sidebar-collapse-btn').on('click', lambda: ui.run_javascript(''' | |
| const html = document.documentElement; | |
| const icon = document.querySelector('.sidebar-collapse-icon'); | |
| html.classList.toggle('sidebar-collapsed'); | |
| const isCollapsed = html.classList.contains('sidebar-collapsed'); | |
| if (isCollapsed) { | |
| icon.textContent = 'chevron_right'; | |
| localStorage.setItem('sidebarCollapsed', 'true'); | |
| } else { | |
| icon.textContent = 'chevron_left'; | |
| localStorage.setItem('sidebarCollapsed', 'false'); | |
| } | |
| ''')): | |
| material_icon('chevron_left', 'sidebar-collapse-icon') | |
| # Navigation | |
| with ui.element('nav').classes('nav-menu'): | |
| for item_id, icon, label, href in nav_items: | |
| is_active = item_id == active_page | |
| classes = 'nav-item active' if is_active else 'nav-item' | |
| with ui.element('a').classes(classes).props(f'href="{href}"' if href != '#' else ''): | |
| material_icon(icon) | |
| ui.label(label).classes('text-sm font-medium nav-item-label') | |
| # Bottom section | |
| with ui.element('div').classes('flex flex-col gap-1'): | |
| for item_id, icon, label, href in bottom_items: | |
| with ui.element('a').classes('nav-item').props(f'href="{href}"' if href != '#' else ''): | |
| material_icon(icon) | |
| ui.label(label).classes('text-sm font-medium nav-item-label') | |
| # Dark mode toggle | |
| dark_mode_toggle() | |
| # Logout (if callback provided) | |
| if on_logout: | |
| with ui.element('div').classes('nav-item').on('click', on_logout): | |
| material_icon('logout') | |
| ui.label('Logout').classes('text-sm font-medium nav-item-label') | |
| def base_layout(active_page: str = 'rubrics', on_logout=None): | |
| """ | |
| Context manager for the base app layout with sidebar. | |
| Usage: | |
| with base_layout(active_page='rubrics', on_logout=handle_logout): | |
| # Your page content here | |
| ui.label('Hello World') | |
| Args: | |
| active_page: Current active page for sidebar highlighting | |
| on_logout: Callback function for logout action | |
| """ | |
| # Add required resources | |
| add_head_resources() | |
| # Main app shell | |
| with ui.element('div').classes('app-shell'): | |
| # Sidebar | |
| sidebar(active_page=active_page, on_logout=on_logout) | |
| # Main content area | |
| with ui.element('main').classes('main-content'): | |
| with ui.element('div').classes('content-wrapper') as content: | |
| yield content | |
| def breadcrumbs(items: list[tuple[str, str]]): | |
| """ | |
| Create a breadcrumb navigation. | |
| Args: | |
| items: List of (label, href) tuples. Last item is current page (no link). | |
| Example: | |
| breadcrumbs([ | |
| ('Rubrics', '/'), | |
| ('History 101', '/project/123'), | |
| ('Define Prompt Groups', None) # Current page | |
| ]) | |
| """ | |
| with ui.element('div').classes('breadcrumbs'): | |
| for i, (label, href) in enumerate(items): | |
| is_last = i == len(items) - 1 | |
| if is_last or href is None: | |
| ui.label(label).classes('breadcrumb-current') | |
| else: | |
| ui.link(label, href).classes('breadcrumb-link') | |
| ui.label('/').classes('breadcrumb-separator') | |
| def page_header(title: str, subtitle: str = None, action_button: tuple = None): | |
| """ | |
| Create a page header with title, optional subtitle, and optional action button. | |
| Args: | |
| title: Main page title | |
| subtitle: Optional subtitle/description | |
| action_button: Optional tuple of (label, callback) for action button | |
| """ | |
| with ui.element('div').classes('page-header'): | |
| with ui.element('div').classes('flex flex-col gap-2'): | |
| ui.html(f'<p class="page-title">{title}</p>', sanitize=False) | |
| if subtitle: | |
| ui.html(f'<p class="page-subtitle">{subtitle}</p>', sanitize=False) | |
| if action_button: | |
| label, callback = action_button | |
| ui.button(label, on_click=callback).classes('btn btn-secondary') | |