πŸ“± App Pages

Pre-built page layouts for authenticated app experiences (login, dashboard, admin).

🎯 Overview

Category Components Purpose
πŸ” Auth LoginScreen Split-screen OAuth login with brand customization
πŸ“ Layout TopLayout Top-nav-only app shell with centered or full-width content

Note: For sidebar-based layouts with navigation rail, use Layout from fh_matui.components.

Code
from fasthtml.jupyter import *
from IPython.display import HTML, Markdown, Image
import socket
import time
import subprocess
import importlib

# Force reload to pick up latest changes
import fh_matui.foundations
import fh_matui.core
import fh_matui.components
importlib.reload(fh_matui.foundations)
importlib.reload(fh_matui.core)
importlib.reload(fh_matui.components)
from fh_matui.components import *

def kill_process_on_port(port):
    """Kill any process using the specified port on Windows"""
    try:
        # Find process using the port
        result = subprocess.run(
            f'netstat -ano | findstr :{port}',
            shell=True, capture_output=True, text=True
        )
        
        if result.stdout:
            # Extract PID from netstat output
            lines = result.stdout.strip().split('\n')
            for line in lines:
                if 'LISTENING' in line:
                    pid = line.strip().split()[-1]
                    subprocess.run(f'taskkill /PID {pid} /F', shell=True, capture_output=True)
                    print(f"βœ“ Killed process {pid} on port {port}")
                    time.sleep(0.5)
                    return True
        return False
    except Exception as e:
        print(f"⚠ Could not kill process on port {port}: {e}")
        return False

def find_available_port(start_port=3333, max_attempts=10):
    """Find an available port starting from start_port"""
    for port in range(start_port, start_port + max_attempts):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.bind(('', port))
                return port
            except OSError:
                continue
    raise RuntimeError(f"Could not find an available port in range {start_port}-{start_port+max_attempts}")

# Stop existing server if running
if 'server' in globals(): 
    try:
        server.stop()
        time.sleep(0.5)
    except:
        pass

# Try to kill any process on preferred port, then find available port
preferred_port = 9998
kill_process_on_port(preferred_port)
port = find_available_port(preferred_port)

app = FastHTML(hdrs=(MatTheme.blue.headers(title="fastmaterial", mode="dark")))
rt = app.route

try:
    server = JupyUvi(app, port=port)
    preview = partial(HTMX, app=app, port=port)
    print(f"βœ“ Server running on port {port}")
except Exception as e:
    print(f"βœ— Failed to start server: {e}")
    raise
βœ“ Server running on port 9998

πŸ” Login Page

Component Purpose
LoginScreen Split-screen OAuth login with configurable branding

Features: OAuth provider buttons, customizable left panel, responsive layout (stacks on mobile)


source

LoginScreen


def LoginScreen(
    title:str='Sign In', subtitle:str='Choose your preferred sign-in method', providers:NoneType=None,
    left_slot:NoneType=None, logo_src:NoneType=None,
    brand_name:NoneType=None, # Brand name displayed at top-left of left panel
    quotes:NoneType=None, # List of quotes to rotate (uses defaults if None, pass [] to disable)
    color_cycle:bool=True, # Enable color cycling animation
    testimonial_text:NoneType=None, # Legacy: single testimonial (use quotes instead)
    brand_bg_cls:str='primary', # Legacy: fallback if gradient fails
    left_cols:int=9, # Number of columns for left side (out of 12)
    cls:str='', kwargs:VAR_KEYWORD
):

A configurable Split Login Screen with dynamic branding.

Features: - Hero-style gradient background (same as landing page) - Brand name + logo at top-left corner - Rotating inspirational quotes in center (pure CSS, hidden on mobile) - Optional color cycling through BeerCSS theme colors (minimal JS)

Args: title: Sign-in form title subtitle: Sign-in form subtitle providers: List of OAuth provider dicts [{label, icon, href, cls}, …] left_slot: Custom content for left panel (overrides default branding) logo_src: URL/path to logo image brand_name: Brand name displayed at top-left corner quotes: List of quote strings to rotate. Defaults to inspirational quotes. Pass empty list [] to disable quotes entirely. color_cycle: Enable gradient color cycling (default True) left_cols: Grid columns for left panel (out of 12). Default 9 = 75%

Example: LoginScreen( brand_name=β€œMyApp”, logo_src=β€œ/static/logo.svg”, quotes=[ β€œYour custom quote here β€” Author”, β€œAnother inspiring message β€” Source”, ], )

Code
preview(LoginScreen())
Code
@app.get("/test-login")
def login():
    return LoginScreen()

πŸ“ TopLayout

A simple top-navigation-only app shell β€” no sidebar, no navigation rail.

Use this when your app navigates via top navbar links (e.g. settings pages, data tables, checkout flows, or analytics dashboards).

Parameter Default Purpose
nav_bar None A NavBar(...) instance for top navigation
main_id 'main-content' ID for the content area (use as hx_target)
main_bg 'surface' Background class for the main content area

Content always stretches full-width inside the <main> area.

TopLayout


def TopLayout(
    content:VAR_POSITIONAL, nav_bar:NoneType=None, main_id:str='main-content', main_bg:str='surface',
    kwargs:VAR_KEYWORD
):

Top-navigation-only app shell β€” full-width content below a sticky NavBar.

Returns (nav, main) as a tuple so they render as sibling elements directly under <body>. This is required for BeerCSS’s native layout engine.

Configure the NavBar with sticky=True, blur='small-blur', and hx_swap='outerHTML' for the recommended glass-effect navigation.

The <main> element has padding-top: 4.5rem (navbar height) to prevent content from being hidden under the sticky navbar, with iOS safe-area support.

Use [TopContent](https://abhisheksreesaila.github.io/fh-matui/app_pages.html#topcontent) to wrap HTMX partial responses.

Args: content: Page content (children) nav_bar: A NavBar(...) instance. Configure sticky/blur/hx_swap on NavBar directly. main_id: ID for the <main> content area (default 'main-content') main_bg: Background class for the main area (default 'surface')

Example::

TopLayout(
    H1("Dashboard"), DashboardGrid(),
    nav_bar=NavBar(
        A("Home", href="/"),
        brand=H5("MyApp"),
        sticky=True,
        blur='small-blur',
        hx_swap='outerHTML'
    ),
)

Route pattern (use TopContent for HTMX partials)::

@rt("/dashboard")
def dashboard(req):
    page = DashboardGrid()
    if 'HX-Request' in req.headers:
        return TopContent(page)
    return TopLayout(page, nav_bar=my_navbar())

source

TopContent


def TopContent(
    content:VAR_POSITIONAL, main_id:str='main-content', main_bg:str='surface'
):

Wrap content for HTMX partial responses inside a TopLayout.

Returns a full-width <main> element with padding-top offset for sticky navbars. Use with hx-swap="outerHTML" on the NavBar.

If content is already a <main> element, returns it directly to prevent double-wrapping.

Args: content: Page content (children) main_id: Must match the main_id used in TopLayout (default 'main-content') main_bg: Background class (default 'surface')

Example::

@rt("/dashboard")
def get(req):
    page = dashboard_content()
    if 'HX-Request' in req.headers:
        return TopContent(page)
    return TopLayout(page, nav_bar=my_navbar())
Code
# --- Working TopLayout example with HTMX SPA navigation ---

# 1. Shared navbar β€” blue with large-blur glass effect
def my_navbar():
    return NavBar(
        A("Dashboard", href="/dashboard"),
        A("Settings", href="/settings"),
        A("Profile", href="/profile"),
        brand=H5("MyApp", cls="bold"),
        sticky=True,
        blur='large-blur',
        hx_swap='outerHTML',
        cls="primary"
    )

# 2. Helper: center content using a 3-column grid (empty | content | empty)
def centered_page(*content):
    """Wrap content in a 3-col grid so it sits in the center column.
    s12 = full-width on mobile, l6 = 6/12 on desktop (centered)."""
    return Grid(
        GridCell(span="s0 m2 l3"),           # left spacer (hidden on small)
        GridCell(*content, span="s12 m8 l6"), # center content
        GridCell(span="s0 m2 l3"),           # right spacer (hidden on small)
    )

# 3. Page content functions
def dashboard_content():
    """Full-width β€” no centering grid, content fills the page."""
    return Div(
        H3("Analytics Dashboard"),
        Div(
            P("Full-width content area β€” stretches edge to edge", cls="center-align"),
            cls="primary-container padding round",
            style="min-height:60px;"
        ),
        Grid(
            Card(H5("Revenue"), P("$12,345"), cls="padding"),
            Card(H5("Users"), P("1,234"), cls="padding"),
            Card(H5("Orders"), P("567"), cls="padding"),
            Card(H5("Conversion"), P("4.2%"), cls="padding"),
            cols=4
        ),
    )

def settings_content():
    """Centered β€” uses the 3-col grid trick for readability."""
    return centered_page(
        H3("Account Settings"),
        P("Update your profile and preferences."),
        Card(
            LabelInput(label="Display Name", id="name"),
            LabelInput(label="Email", id="email", input_type="email"),
            Button("Save", cls=ButtonT.primary),
            cls="padding"
        )
    )

def profile_content():
    """Centered β€” uses the 3-col grid trick for readability."""
    return centered_page(
        H3("Profile"),
        Card(
            DivHStacked(
                I("person", cls="circle extra primary-container"),
                DivVStacked(H5("John Doe"), P("john@example.com", cls="small-text"))
            ),
            cls="padding"
        )
    )

# 4. Routes β€” all full-width <main>, centering handled by content itself
@rt("/dashboard")
def get(req):
    content = dashboard_content()
    if 'HX-Request' in req.headers:
        return TopContent(content)
    return TopLayout(content, nav_bar=my_navbar())

@rt("/settings")
def get(req):
    content = settings_content()
    if 'HX-Request' in req.headers:
        return TopContent(content)
    return TopLayout(content, nav_bar=my_navbar())

@rt("/profile")
def get(req):
    content = profile_content()
    if 'HX-Request' in req.headers:
        return TopContent(content)
    return TopLayout(content, nav_bar=my_navbar())

# 5. Preview β€” full-width dashboard
preview(TopLayout(dashboard_content(), nav_bar=my_navbar()))