from cjm_fasthtml_tailwind.core.base import combine_classes
from cjm_fasthtml_tailwind.utilities.spacing import *
from cjm_fasthtml_tailwind.utilities.sizing import *
from cjm_fasthtml_tailwind.utilities.layout import *
from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import *
from cjm_fasthtml_tailwind.utilities.backgrounds import bg
from cjm_fasthtml_tailwind.utilities.typography import font_size, text_colortesting
Standardized test page creation for Jupyter notebooks with FastHTML
Test App Creation
A standardized way to create test apps in Jupyter notebooks:
create_test_app
def create_test_app(
debug:bool=True, # Enable debug mode
)->tuple: # Tuple containing (app, rt) - FastHTML app instance and route decorator
Create a standardized test app for Jupyter notebooks with Tailwind.
Test Page Wrapper
A wrapper for creating consistent test pages:
create_test_page
def create_test_page(
title:str, # Page title
content:VAR_POSITIONAL, # Page content elements
container:bool=True, # Wrap in container
custom_theme_names:Optional=None, # Custom themes for selector
)->functools.partial(<function ft_hx at 0x7f61d021d080>, 'div'): # Div containing complete page layout with navbar and content
Create a standardized test page layout with optional theme selector.
Jupyter Notebook Utilities
Helper functions for working with FastHTML in Jupyter:
start_test_server
def start_test_server(
app:FastHTML, # FastHTML app instance created by create_test_app or fast_app
port:int=8000, # Port
)->JupyUvi: # JupyUvi server instance for Jupyter notebook testing
Start a test server and return the JupyUvi instance.
Usage: server = start_test_server(app) HTMX() # Display the app
# Later, in another cell:
server.stop()
Example Usage
Here’s how to use the testing utilities in a notebook:
# Create a test app with default settings
app, rt = create_test_app()
padding_val = 2
gap_val = 2
# Define a test route
@rt
def index():
return create_test_page(
"Tailwind Test Page",
Div(
# Header spans full width
Header("Dashboard", cls=combine_classes(col_span.full, p(padding_val), bg.blue._500, text_color.white)),
# Sidebar
Aside("Sidebar", cls=combine_classes(row_span(2), p(padding_val), bg.gray._200)),
# Main content
Main("Main Content", cls=combine_classes(col_span(2), p(padding_val))),
# Stats cards
Div("Stat 1", cls= combine_classes(p(padding_val), bg.green._100)),
Div("Stat 2", cls= combine_classes(p(padding_val), bg.yellow._100)),
cls=combine_classes(
grid_display,
grid_cols(3),
grid_rows(3),
gap(gap_val),
h.screen
)
)
)
# Start the server
server = start_test_server(app)
HTMX()# Stop the server when done
server.stop()Comprehensive Example - Modern Dashboard Layout
A more comprehensive example demonstrating various Tailwind utilities to create a modern dashboard:
# Import all utilities we'll use
from fasthtml.common import *
from cjm_fasthtml_tailwind.utilities.accessibility import not_sr_only, sr_only
from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import (
auto_cols, auto_rows, basis, col, col_end, col_span, col_start,
content, flex, flex_between, flex_center, flex_col_center,
flex_direction, flex_wrap, gap, grid_center, grid_cols, grid_flow,
grid_rows, grow, items, justify, justify_items, justify_self,
order, place_content, place_items, place_self, responsive_grid,
row, row_end, row_span, row_start, self_align, shrink
)
from cjm_fasthtml_tailwind.utilities.layout import (
aspect, bottom, box, box_decoration, break_util, center_absolute,
clear, columns, display_tw, end, float_tw, full_bleed, inset,
isolation, left, object_fit, object_position,
overflow, overscroll, position, right, stack_context,
start, sticky_top, top, visibility, z
)
from cjm_fasthtml_tailwind.utilities.sizing import (
container, full_screen, full_size, h, max_h, max_w, min_h,
min_w, size, size_util, square, w
)
from cjm_fasthtml_tailwind.utilities.spacing import (
m, margin, me, ms, p, pad, pe, ps, space
)
from cjm_fasthtml_tailwind.core.base import combine_classes
from cjm_fasthtml_tailwind.core.resources import get_tailwind_headers# Create a new test app
from cjm_fasthtml_tailwind.utilities.typography import font_size, font_weight, font_family, text_color
from cjm_fasthtml_tailwind.utilities.backgrounds import bg
from cjm_fasthtml_tailwind.utilities.borders import rounded, border
from cjm_fasthtml_tailwind.utilities.effects import shadow
from cjm_fasthtml_tailwind.utilities.transitions_and_animation import transition
app, rt = create_test_app()
@rt
def index():
# Navbar component
navbar = Nav(
Div(
# Logo section
Div(
H1("Dashboard", cls=combine_classes(font_size.xl, font_weight.bold, text_color.white)),
cls=combine_classes(flex_display, items.center)
),
# Navigation links
Div(
A("Home", href="#", cls=combine_classes(p.x(4), p.y(2), text_color.white, bg.blue._700.hover, rounded.full)),
A("Analytics", href="#", cls=combine_classes(p.x(4), p.y(2), text_color.white, bg.blue._700.hover, rounded.full)),
A("Reports", href="#", cls=combine_classes(p.x(4), p.y(2), text_color.white, bg.blue._700.hover, rounded.full)),
A("Settings", href="#", cls=combine_classes(p.x(4), p.y(2), text_color.white, bg.blue._700.hover, rounded.full)),
cls=combine_classes(flex_display, gap(2))
),
# User profile section
Div(
Div(
Span("John Doe", cls=text_color.white),
Div(cls=combine_classes(square(10), bg.white, rounded.full)),
cls=combine_classes(flex_display, items.center, gap(3))
),
cls=combine_classes(m.l.auto)
),
cls=combine_classes(
flex_between(),
container(),
m.x.auto,
p.x(4)
)
),
cls=combine_classes(
w.full,
bg.blue._600,
sticky_top(),
z(50)
)
)
# Main layout with sidebar and content
main_layout = Div(
# Sidebar
Aside(
# Sidebar header
H2("Menu", cls=combine_classes(p(4), font_size.lg, font_weight.semibold, border.b())),
# Menu items
Ul(
Li(
A(
I(cls=combine_classes("fas fa-chart-line", m.r(0.5))),
"Dashboard",
href="#",
cls=combine_classes(
flex_display,
items.center,
p(4),
bg.gray._100.hover,
transition.colors
)
)
),
Li(
A(
I(cls=combine_classes("fas fa-users", m.r(0.5))),
"Users",
href="#",
cls=combine_classes(
flex_display,
items.center,
p(4),
bg.gray._100.hover,
transition.colors
)
)
),
Li(
A(
I(cls=combine_classes("fas fa-cog", m.r(0.5))),
"Settings",
href="#",
cls=combine_classes(
flex_display,
items.center,
p(4),
bg.gray._100.hover,
transition.colors
)
)
),
cls=space.y(1)
),
cls=combine_classes(
w(64),
min_h.screen,
bg.white,
border.r(),
position.sticky,
top(16), # Account for navbar height
overflow.y.auto
)
),
# Main content area
Main(
# Page header
Header(
H1("Analytics Dashboard", cls=combine_classes(font_size._3xl, font_weight.bold)),
P("Last updated: " + "December 2024", cls=text_color.gray._600),
cls=combine_classes(p(6), bg.white, rounded.lg, shadow(), m.b(6))
),
# Stats grid
Div(
# Stat card 1
Div(
Div(
H3("Total Users", cls=combine_classes(text_color.gray._600, font_size.sm)),
P("12,345", cls=combine_classes(font_size._3xl, font_weight.bold, text_color.blue._600)),
P("↑ 12.5%", cls=combine_classes(text_color.green._500, font_size.sm)),
cls=pad(x=6, y=4)
),
cls=combine_classes(bg.white, rounded.lg, shadow(), overflow.hidden)
),
# Stat card 2
Div(
Div(
H3("Revenue", cls=combine_classes(text_color.gray._600, font_size.sm)),
P("$45,678", cls=combine_classes(font_size._3xl, font_weight.bold, text_color.green._600)),
P("↑ 8.2%", cls=combine_classes(text_color.green._500, font_size.sm)),
cls=pad(x=6, y=4)
),
cls=combine_classes(bg.white, rounded.lg, shadow(), overflow.hidden)
),
# Stat card 3
Div(
Div(
H3("Active Sessions", cls=combine_classes(text_color.gray._600, font_size.sm)),
P("892", cls=combine_classes(font_size._3xl, font_weight.bold, text_color.purple._600)),
P("↓ 3.1%", cls=combine_classes(text_color.red._500, font_size.sm)),
cls=pad(x=6, y=4)
),
cls=combine_classes(bg.white, rounded.lg, shadow(), overflow.hidden)
),
# Stat card 4
Div(
Div(
H3("Conversion Rate", cls=combine_classes(text_color.gray._600, font_size.sm)),
P("3.42%", cls=combine_classes(font_size._3xl, font_weight.bold, text_color.orange._600)),
P("↑ 0.8%", cls=combine_classes(text_color.green._500, font_size.sm)),
cls=pad(x=6, y=4)
),
cls=combine_classes(bg.white, rounded.lg, shadow(), overflow.hidden)
),
cls=responsive_grid(
mobile=1,
tablet=2,
desktop=4,
gap_size=6
)
),
# Charts section
Div(
# Main chart
Div(
H2("Revenue Overview", cls=combine_classes(font_size.xl, font_weight.semibold, m.b(4))),
Div(
"Chart placeholder - Revenue trends over time",
cls=combine_classes(
h(64),
bg.gray._100,
rounded(),
flex_center()
)
),
cls=combine_classes(
col_span(2),
bg.white,
rounded.lg,
shadow(),
p(6)
)
),
# Side panel
Div(
H2("Top Products", cls=combine_classes(font_size.xl, font_weight.semibold, m.b(4))),
Ul(
Li("Product A - $12,345", cls=pad(y=2)),
Li("Product B - $10,234", cls=pad(y=2)),
Li("Product C - $8,912", cls=pad(y=2)),
Li("Product D - $7,234", cls=pad(y=2)),
Li("Product E - $5,123", cls=pad(y=2)),
cls=space.y(2)
),
cls=combine_classes(
bg.white,
rounded.lg,
shadow(),
p(6)
)
),
cls=combine_classes(
grid_display,
grid_cols(3),
gap(6),
m.t(6)
)
),
# Table section
Div(
H2("Recent Transactions", cls=combine_classes(font_size.xl, font_weight.semibold, m.b(4))),
Div(
Table(
Thead(
Tr(
Th("ID", cls=pad(x=6, y=3)),
Th("Customer", cls=pad(x=6, y=3)),
Th("Amount", cls=pad(x=6, y=3)),
Th("Status", cls=pad(x=6, y=3)),
Th("Date", cls=pad(x=6, y=3)),
cls=bg.gray._50
)
),
Tbody(
Tr(
Td("#001", cls=pad(x=6, y=4)),
Td("John Smith", cls=pad(x=6, y=4)),
Td("$234.50", cls=pad(x=6, y=4)),
Td(
Span("Completed", cls=combine_classes(p.x(2), p.y(1), bg.green._100, text_color.green._800, rounded.full, font_size.sm)),
cls=pad(x=6, y=4)
),
Td("Dec 1, 2024", cls=pad(x=6, y=4)),
cls=border.t()
),
Tr(
Td("#002", cls=pad(x=6, y=4)),
Td("Jane Doe", cls=pad(x=6, y=4)),
Td("$567.80", cls=pad(x=6, y=4)),
Td(
Span("Pending", cls=combine_classes(p.x(2), p.y(1), bg.yellow._100, text_color.yellow._800, rounded.full, font_size.sm)),
cls=pad(x=6, y=4)
),
Td("Dec 1, 2024", cls=pad(x=6, y=4)),
cls=border.t()
),
Tr(
Td("#003", cls=pad(x=6, y=4)),
Td("Bob Johnson", cls=pad(x=6, y=4)),
Td("$123.45", cls=pad(x=6, y=4)),
Td(
Span("Failed", cls=combine_classes(p.x(2), p.y(1), bg.red._100, text_color.red._800, rounded.full, font_size.sm)),
cls=pad(x=6, y=4)
),
Td("Dec 1, 2024", cls=pad(x=6, y=4)),
cls=border.t()
)
),
cls=str(min_w.full)
),
cls=overflow.x.auto
),
cls=combine_classes(
bg.white,
rounded.lg,
shadow(),
p(6),
m.t(6)
)
),
cls=combine_classes(
flex(1),
p(6),
bg.gray._50,
min_h.screen
)
),
cls=combine_classes(flex_display, min_h.screen)
)
return Div(
navbar,
main_layout,
cls=font_family.sans
)
# Start the server
server = start_test_server(app, port=8001)
HTMX(port=8001)# Stop the server when done
server.stop()Comprehensive Example - Card-Based Layout
Another example showing a card-based layout with various utility combinations:
# Create another test app for the card-based layout
from cjm_fasthtml_tailwind.utilities.backgrounds import bg_linear, from_color, to_color
from cjm_fasthtml_tailwind.utilities.typography import font_size, font_family, font_weight, text_color, text_align, line_through
from cjm_fasthtml_tailwind.utilities.effects import shadow, ring, ring_color, shadow_color
from cjm_fasthtml_tailwind.utilities.transitions_and_animation import transition, ease, duration, animate
from cjm_fasthtml_tailwind.utilities.borders import outline, outline_offset, outline_style
app2, rt2 = create_test_app()
@rt2
def index():
# Header with centered content
header = Header(
Div(
H1("Product Showcase", cls=combine_classes(font_size._4xl, font_weight.bold, text_color.white)),
P("Discover our amazing products", cls=combine_classes(font_size.xl, text_color.gray._200)),
cls=combine_classes(
container(),
m.x.auto,
pad(x=4, y=16),
text_align.center
)
),
cls=combine_classes(bg_linear.to_r, from_color.blue._600, to_color.purple._600)
)
# Filter section
filter_section = Section(
Div(
# Filter title
H2("Filter by Category", cls=combine_classes(font_size.lg, font_weight.semibold, m.b(4))),
# Filter buttons
Div(
Button("All", cls=combine_classes(
p.x(6), p.y(2),
bg.blue._600, text_color.white, rounded.full,
bg.blue._700.hover, transition.colors
)),
Button("Electronics", cls=combine_classes(
p.x(6), p.y(2),
bg.gray._200, text_color.gray._700, rounded.full,
bg.gray._300.hover, transition.colors
)),
Button("Clothing", cls=combine_classes(
p.x(6), p.y(2),
bg.gray._200, text_color.gray._700, rounded.full,
bg.gray._300.hover, transition.colors
)),
Button("Books", cls=combine_classes(
p.x(6), p.y(2),
bg.gray._200, text_color.gray._700, rounded.full,
bg.gray._300.hover, transition.colors
)),
cls=combine_classes(
flex_display,
flex_wrap.wrap,
gap(3),
justify.center
)
),
cls=combine_classes(
container(),
m.x.auto,
pad(x=4, y=8)
)
),
cls=combine_classes(bg.gray._50, border.b())
)
# Product cards grid
products_grid = Main(
Div(
# Product Card 1
Article(
# Image placeholder with aspect ratio
Div(
Img(src="https://via.placeholder.com/400x300",
alt="Product 1",
cls=combine_classes(w.full, h.full, object_fit.cover)),
cls=combine_classes(aspect.video, overflow.hidden, bg.gray._200)
),
# Card content
Div(
# Product title and price
Div(
H3("Premium Headphones", cls=combine_classes(font_size.lg, font_weight.semibold)),
P("$199.99", cls=combine_classes(font_size.xl, font_weight.bold, text_color.blue._600)),
cls=combine_classes(flex_display, justify.between, items.start, m.b(2))
),
# Product description
P("High-quality wireless headphones with noise cancellation",
cls=combine_classes(text_color.gray._600, font_size.sm, m.b(4))),
# Rating
Div(
Span("⭐⭐⭐⭐⭐", cls=text_color.yellow._400),
Span("(4.8)", cls=combine_classes(text_color.gray._500, font_size.sm, m.l(2))),
cls=combine_classes(flex_display, items.center, m.b(4))
),
# Action buttons
Div(
Button("Add to Cart", cls=combine_classes(
flex(1),
pad(x=4, y=2),
bg.blue._600, text_color.white, rounded(),
bg.blue._700.hover, transition.colors
)),
Button("♥", cls=combine_classes(
square(10),
bg.gray._100, rounded(),
bg.red._100.hover, text_color.red._600.hover, transition.colors,
m.l(2)
)),
cls=combine_classes(flex_display, items.center)
),
cls=p(6)
),
cls=combine_classes(
bg.white, rounded.lg, shadow.md,
overflow.hidden,
shadow.xl.hover, transition.shadow,
flex_display,
flex_direction.col
)
),
# Product Card 2 - Featured
Article(
# Featured badge
Div(
Span("Featured", cls=combine_classes(p.x(3), p.y(1), bg.yellow._400, text_color.yellow._900, font_size.sm, font_weight.semibold, rounded.full)),
cls=combine_classes(position.absolute, top(4), right(4), z(10))
),
# Image with overlay
Div(
Img(src="https://via.placeholder.com/400x300",
alt="Product 2",
cls=combine_classes(w.full, h.full, object_fit.cover)),
# Gradient overlay
Div(cls=combine_classes(
position.absolute,
inset(0),
bg_linear.to_t, from_color.black.opacity(50), to_color.transparent
)),
cls=combine_classes(
aspect.video,
overflow.hidden,
bg.gray._200,
position.relative
)
),
# Card content
Div(
H3("Smart Watch Pro", cls=combine_classes(font_size.lg, font_weight.semibold)),
P("$349.99", cls=combine_classes(font_size.xl, font_weight.bold, text_color.blue._600, m.b(2))),
P("Advanced fitness tracking with GPS and heart rate monitor",
cls=combine_classes(text_color.gray._600, font_size.sm, m.b(4))),
# Features list
Ul(
Li("✓ Water resistant", cls=combine_classes(font_size.sm, text_color.gray._600)),
Li("✓ 7-day battery life", cls=combine_classes(font_size.sm, text_color.gray._600)),
Li("✓ Multiple sport modes", cls=combine_classes(font_size.sm, text_color.gray._600)),
cls=space.y(1)
),
# CTA button
Button("View Details", cls=combine_classes(
w.full,
pad(x=4, y=3),
m.t(4),
bg_linear.to_r, from_color.purple._600, to_color.blue._600,
text_color.white, rounded(),
from_color.purple._700.hover, to_color.blue._700.hover, transition.all
)),
cls=p(6)
),
cls=combine_classes(
bg.white, rounded.lg, shadow.lg,
overflow.hidden,
position.relative,
shadow._2xl.hover, transition.shadow, shadow_color.amber._400.hover,
ease._in, duration._500,
flex_display,
flex_direction.col
)
),
# Product Card 3 - Sale item
Article(
# Sale badge
Div(
Span("-30%", cls=combine_classes(p.x(3), p.y(1), bg.red._500, text_color.white, font_size.sm, font_weight.bold, rounded.full)),
cls=combine_classes(position.absolute, top(4), left(4), z(10))
),
Div(
Img(src="https://via.placeholder.com/400x300",
alt="Product 3",
cls=combine_classes(w.full, h.full, object_fit.cover)),
cls=combine_classes(aspect.video, overflow.hidden, bg.gray._200, position.relative)
),
Div(
H3("Laptop Backpack", cls=combine_classes(font_size.lg, font_weight.semibold)),
Div(
P("$69.99", cls=combine_classes(font_size.xl, font_weight.bold, text_color.red._600)),
P("$99.99", cls=combine_classes(font_size.sm, text_color.gray._400, line_through)),
cls=combine_classes(flex_display, items.baseline, gap(2), m.b(2))
),
P("Durable backpack with laptop compartment and USB charging port",
cls=combine_classes(text_color.gray._600, font_size.sm, m.b(4))),
# Stock indicator
Div(
Div(cls=combine_classes(size(2), bg.green._500, rounded.full)),
Span("In Stock", cls=combine_classes(font_size.sm, text_color.gray._600)),
cls=combine_classes(flex_display, items.center, gap(2), m.b(4))
),
Button("Quick Buy", cls=combine_classes(
w.full,
pad(x=4, y=2),
bg.red._600, text_color.white, rounded(),
bg.red._700.hover, transition.colors
)),
cls=p(6)
),
cls=combine_classes(
bg.white, rounded.lg, shadow.md,
overflow.hidden,
position.relative,
shadow.xl.hover, transition.shadow,
flex_display,
flex_direction.col
)
),
# More product cards...
*[
Article(
Div(
Img(src="https://via.placeholder.com/400x300",
alt=f"Product {i}",
cls=combine_classes(w.full, h.full, object_fit.cover)),
cls=combine_classes(aspect.video, overflow.hidden, bg.gray._200)
),
Div(
H3(f"Product {i}", cls=combine_classes(font_size.lg, font_weight.semibold)),
P(f"${49.99 + i * 10}", cls=combine_classes(font_size.xl, font_weight.bold, text_color.blue._600, m.b(2))),
P("Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
cls=combine_classes(text_color.gray._600, font_size.sm, m.b(4))),
Button("Add to Cart", cls=combine_classes(
w.full,
pad(x=4, y=2),
bg.blue._600, text_color.white, rounded(),
bg.blue._700.hover, transition.colors
)),
cls=p(6)
),
cls=combine_classes(
bg.white, rounded.lg, shadow.md,
overflow.hidden,
shadow.xl.hover, transition.shadow,
flex_display,
flex_direction.col
)
) for i in range(4, 7)
],
cls=responsive_grid(
mobile=1,
tablet=2,
desktop=3,
gap_size=8
)
),
cls=combine_classes(
container(),
m.x.auto,
pad(x=4, y=12)
)
)
# Newsletter section
newsletter = Section(
Div(
# Content wrapper
Div(
H2("Stay Updated", cls=combine_classes(font_size._2xl, font_weight.bold, text_color.white, m.b(2))),
P("Get the latest products and exclusive offers", cls=combine_classes(text_color.gray._200, m.b(6))),
# Form
Form(
Div(
Input(
type="email",
placeholder="Enter your email",
cls=combine_classes(
flex(1),
p(3),
bg.white, rounded.l.lg,
outline_style.none.focus, ring(2).focus, ring_color.blue._400.focus
)
),
Button(
"Subscribe",
type="submit",
cls=combine_classes(
p.x(8), p(3),
bg.yellow._400, text_color.gray._900, font_weight.semibold, rounded.r.lg,
bg.yellow._300.hover, transition.colors
)
),
cls=combine_classes(flex_display, max_w.md, m.x.auto)
),
cls=str(w.full)
),
cls=combine_classes(
max_w._2xl,
m.x.auto,
text_align.center
)
),
cls=combine_classes(
container(),
m.x.auto,
pad(x=4, y=16)
)
),
cls=combine_classes(bg_linear.to_r, from_color.blue._600, to_color.purple._600)
)
# Footer
footer = Footer(
Div(
P("© 2024 Product Showcase. All rights reserved.",
cls=combine_classes(text_color.gray._600, text_align.center)),
cls=combine_classes(
container(),
m.x.auto,
pad(x=4, y=8)
)
),
cls=combine_classes(bg.gray._100, border.t())
)
return Div(
header,
filter_section,
products_grid,
newsletter,
footer,
cls=combine_classes(min_h.screen, bg.gray._50)
)
# Start the server
server2 = start_test_server(app2, port=8002)
HTMX(port=8002)# Stop the server when done
server2.stop()