itslikethisnow's picture
lots of updates
8d306f9
"""
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')
@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'<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')