diff --git a/src/air/layouts.py b/src/air/layouts.py index 8b0d8f21d..a6e4dfaf2 100644 --- a/src/air/layouts.py +++ b/src/air/layouts.py @@ -1,9 +1,10 @@ """Tools for building layouts and several simple layouts for quick prototyping.""" +from enum import Enum, auto from typing import Any from .tags import Body, Children, Head, Header, Html, Link, Main, Script, Style -from .tags.models.types import HEAD_TAG_TYPES, AttributeType +from .tags.models.types import FOOTER_TAG_TYPES, HEAD_TAG_TYPES, HEADER_TAG_TYPES, AttributeType def filter_body_tags(tags: tuple) -> list: @@ -24,8 +25,26 @@ def filter_head_tags(tags: tuple) -> list: return [t for t in tags if isinstance(t, HEAD_TAG_TYPES)] +def filter_header_tags(tags: tuple) -> list: + """Given a list of tags, only list the ones that belong in header of an HTML document. + + Returns: + List of tags that belong in the header of an HTML document. + """ + return [t for t in tags if isinstance(t, HEADER_TAG_TYPES)] + + +def filter_footer_tags(tags: tuple) -> list: + """Given a list of tags, only list the ones that belong in footer of an HTML document. + + Returns: + List of tags that belong in the footer of an HTML document. + """ + return [t for t in tags if isinstance(t, FOOTER_TAG_TYPES)] + + def _header(tags: tuple | list) -> Header | str: - """Extracts the air.Header tag from a set of tags. + """Extracts the Header tag from a set of tags. Returns: The Header tag if found, otherwise an empty string. @@ -36,6 +55,140 @@ def _header(tags: tuple | list) -> Header | str: return "" +class MuTheme(Enum): + red = auto() + pumpkin = auto() + orange = auto() + amber = auto() + yellow = auto() + lime = auto() + green = auto() + jade = auto() + cyan = auto() + azure = auto() # default + blue = auto() + indigo = auto() + violet = auto() + purple = auto() + fuchsia = auto() + pink = auto() + sand = auto() + grey = auto() + zinc = auto() + slate = auto() + + +def mucss( + *children: Any, + theme: MuTheme = MuTheme.azure, + force_dark_mode: bool = False, + is_htmx: bool = False, + **kwargs: AttributeType, +) -> Html | Children: + """Renders the basic layout with μCSS and HTMX for quick prototyping. + + 1. At the top level HTML head tags are put in the `` tag + 2. Otherwise everything is put in the `` + a. `Header` is placed above `Main` + a. `Footer` is placed below `Main` + 3. If `is_htmx` is True, then the layout isn't included. This is to support the `hx_boost` + feature of HTMX + + Args: + children: These typically inherit from Tag but can be anything + theme: + force_dark_mode: For forcing dark-colored pages + is_htmx: Whether or not HTMX sent the request from the page + + Returns: + HTML document with MuCSS styling or Children for HTMX partial responses. + + Example: + + import air + + app = Air() + + + @app.page + async def index(request: Request) -> Html | Children: + return layouts.MuCSS( + Title("Home"), + Article( + H1("Welcome to Air"), + P(A("Click to go to Dashboard", href="/dashboard")), + hx_boost="true", + ), + is_htmx=request.htmx.is_hx_request, + ) + + + @app.page + async def dashboard(request: Request) -> Html | Children: + return layouts.MuCSS( + Title("Dashboard"), + Article( + H1("Dashboard"), + P(A("Go home", href="/")), + hx_boost="true", + ), + is_htmx=request.htmx.is_hx_request, + ) + + + if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="127.0.0.1", port=8000) + """ + body_tags = filter_body_tags(filter_body_tags(children)) + head_tags = filter_head_tags(children) + header_tags = filter_header_tags(children) + footer_tags = filter_footer_tags(children) + + if is_htmx: + return Children(Main(*body_tags, class_="container"), *head_tags) + + return Html( + Head( + Link( + rel="stylesheet", + href=f"https://unpkg.com/@digicreon/mucss/dist/mu.{theme}.css", + ), + Style(""" +@media (max-width: 768px) { + body { + font-size: 18px; + line-height: 1.7; + padding: 0 14px; + } + + h1, h2, h3 { + line-height: 1.25; + } + + pre, code { + font-size: 0.9em; + overflow-x: auto; + } +} +"""), + Script( + src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js", + integrity="sha384-Akqfrbj/HpNVo8k11SXBb6TlBWmXXlYQrCSqEWmyKJe+hDm3Z/B2WVG4smwBkRVm", + crossorigin="anonymous", + ), + *head_tags, + ), + Body( + *header_tags, + Main(*body_tags, class_="container"), + *footer_tags, + ), + data_theme="dark" if force_dark_mode else "", + ) + + def mvpcss(*children: Any, is_htmx: bool = False, **kwargs: AttributeType) -> Html | Children: """Renders the basic layout with MVP.css and HTMX for quick prototyping @@ -51,7 +204,7 @@ def mvpcss(*children: Any, is_htmx: bool = False, **kwargs: AttributeType) -> Ht you'll have to create your own layouts. Args: - children: These typically inherit from air.Tag but can be anything + children: These typically inherit from Tag but can be anything is_htmx: Whether or not HTMX sent the request from the page Returns: @@ -61,16 +214,16 @@ def mvpcss(*children: Any, is_htmx: bool = False, **kwargs: AttributeType) -> Ht import air - app = air.Air() + app = Air() @app.page - async def index(request: air.Request) -> air.Html | air.Children: - return air.layouts.mvpcss( - air.Title("Home"), - air.Article( - air.H1("Welcome to Air"), - air.P(air.A("Click to go to Dashboard", href="/dashboard")), + async def index(request: Request) -> Html | Children: + return layouts.mvpcss( + Title("Home"), + Article( + H1("Welcome to Air"), + P(A("Click to go to Dashboard", href="/dashboard")), hx_boost="true", ), is_htmx=request.htmx.is_hx_request, @@ -78,12 +231,12 @@ async def index(request: air.Request) -> air.Html | air.Children: @app.page - async def dashboard(request: air.Request) -> air.Html | air.Children: - return air.layouts.mvpcss( - air.Title("Dashboard"), - air.Article( - air.H1("Dashboard"), - air.P(air.A("Go home", href="/")), + async def dashboard(request: Request) -> Html | Children: + return layouts.mvpcss( + Title("Dashboard"), + Article( + H1("Dashboard"), + P(A("Go home", href="/")), hx_boost="true", ), is_htmx=request.htmx.is_hx_request, @@ -133,7 +286,7 @@ def picocss(*children: Any, is_htmx: bool = False, **kwargs: AttributeType) -> H you'll have to create your own layouts. Args: - children: These typically inherit from air.Tag but can be anything + children: These typically inherit from Tag but can be anything is_htmx: Whether or not HTMX sent the request from the page Returns: @@ -143,16 +296,16 @@ def picocss(*children: Any, is_htmx: bool = False, **kwargs: AttributeType) -> H import air - app = air.Air() + app = Air() @app.page - async def index(request: air.Request) -> air.Html | air.Children: - return air.layouts.picocss( - air.Title("Home"), - air.Article( - air.H1("Welcome to Air"), - air.P(air.A("Click to go to Dashboard", href="/dashboard")), + async def index(request: Request) -> Html | Children: + return layouts.picocss( + Title("Home"), + Article( + H1("Welcome to Air"), + P(A("Click to go to Dashboard", href="/dashboard")), hx_boost="true", ), is_htmx=request.htmx.is_hx_request, @@ -160,12 +313,12 @@ async def index(request: air.Request) -> air.Html | air.Children: @app.page - async def dashboard(request: air.Request) -> air.Html | air.Children: - return air.layouts.picocss( - air.Title("Dashboard"), - air.Article( - air.H1("Dashboard"), - air.P(air.A("Go home", href="/")), + async def dashboard(request: Request) -> Html | Children: + return layouts.picocss( + Title("Dashboard"), + Article( + H1("Dashboard"), + P(A("Go home", href="/")), hx_boost="true", ), is_htmx=request.htmx.is_hx_request, diff --git a/src/air/tags/models/types.py b/src/air/tags/models/types.py index 781bc3469..20d1db2dc 100644 --- a/src/air/tags/models/types.py +++ b/src/air/tags/models/types.py @@ -11,12 +11,7 @@ Script, Style, ) -from .stock import ( - Base, - Link, - Meta, - Title, -) +from .stock import Base, Footer, Header, Link, Meta, Title type Renderable = Annotated[ str | BaseTag | SafeStr | int | float, @@ -71,3 +66,5 @@ class TagDictType(TypedDict): type TagTypes = tuple[type[BaseTag], ...] HEAD_TAG_TYPES: Final[TagTypes] = (Title, Style, Meta, Link, Script, Base) +HEADER_TAG_TYPES: Final[TagTypes] = Header +FOOTER_TAG_TYPES: Final[TagTypes] = Footer