""" 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(''' ''') # Google Fonts - Inter ui.add_head_html('') ui.add_head_html('') ui.add_head_html('') # Material Symbols ui.add_head_html('') # SortableJS for drag and drop ui.add_head_html('') # 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(''' ''') def material_icon(name: str, classes: str = '') -> ui.html: """Helper to create a Material Symbols icon.""" return ui.html(f'{name}', 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('

Rubric AI

', 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') @contextmanager 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'

{title}

', sanitize=False) if subtitle: ui.html(f'

{subtitle}

', sanitize=False) if action_button: label, callback = action_button ui.button(label, on_click=callback).classes('btn btn-secondary')