File size: 8,366 Bytes
dccc925
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d306f9
 
 
 
 
 
 
 
 
 
 
 
 
dccc925
 
 
 
 
 
 
 
 
 
 
 
 
 
8d306f9
 
 
 
 
 
 
 
 
 
 
 
dccc925
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d306f9
dccc925
 
 
 
 
 
 
 
 
 
 
 
 
8d306f9
dccc925
 
 
 
 
 
 
 
 
 
 
 
8d306f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dccc925
 
 
 
 
 
 
 
 
8d306f9
dccc925
 
 
 
 
 
8d306f9
dccc925
 
 
 
 
 
 
 
8d306f9
dccc925
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
"""
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')