Skip to content

tempestweb.presets

Ready-made admin-panel screens, assembled from typed records (NavItem, Kpi, Section, TableColumn, FormField) rather than layout widgets. Import from here when the screen is an archetype — shell, dashboard, listing, form, login.

Guide with examples: Ready-made screens (presets) · Admin console.

tempestweb.presets

Ready-made screens for dashboards, admin panels and internal tools.

A preset is a whole screen you describe with data instead of assembling with widgets. You say what is on it — these nav entries, these KPIs, these columns, these fields — and the preset decides the spacing, the type scale, the grid and the responsive behaviour::

from tempestweb.presets import Kpi, NavItem, admin_shell, dashboard_page

def view(app):
    return admin_shell(
        title="Painel",
        nav=[NavItem("Visão geral", "overview"), NavItem("Usuários", "users")],
        active=app.state.tab,
        on_navigate=lambda value: app.set_state(...),
        body=dashboard_page(
            title="Visão geral",
            kpis=[Kpi("Receita", "R$ 82k", delta="+12%", tone="success")],
        ),
    )

Nothing here measures the viewport. Every breakpoint lives in client/layouts.js, the stylesheet the client injects at mount: the sidebar collapses under 1024px, the KPI row reflows, the table scrolls sideways under a sticky header, and printing drops the chrome. The presets only tag each container with its layout role (data-tw-layout) so those rules can find it. That means the same tree is correct at every width, in all three modes, with no media query of your own — and an inline Style you set still wins over anything the sheet says.

The presets compose the same public components an app would: they are a shortcut, never a wall. Use one for the shell and hand-build the body, replace a section with your own widget, or stop using them entirely — the widgets underneath are the ones you already know.

FormField dataclass

One labelled control in a form page.

Attributes:

Name Type Description
label str

The field's label.

control Widget

The input widget itself — any core/tempestweb field. The preset positions it; it never builds or validates it.

help str | None

Optional hint shown under the control.

error str | None

Optional validation message, shown instead of help when set.

span Span

"full" makes the field take the whole row (a textarea, an address); "auto" lets it share the row with its neighbours.

Source code in tempestweb/presets/models.py
@dataclass(frozen=True, slots=True)
class FormField:
    """One labelled control in a form page.

    Attributes:
        label: The field's label.
        control: The input widget itself — any core/tempestweb field. The preset
            positions it; it never builds or validates it.
        help: Optional hint shown under the control.
        error: Optional validation message, shown instead of ``help`` when set.
        span: ``"full"`` makes the field take the whole row (a textarea, an
            address); ``"auto"`` lets it share the row with its neighbours.
    """

    label: str
    control: Widget
    help: str | None = None
    error: str | None = None
    span: Span = "auto"

FormSection dataclass

A group of related fields under one heading.

Attributes:

Name Type Description
title str

The group heading ("Dados da conta", "Notificações").

fields list[FormField]

The fields in the group, laid out in the responsive form grid.

subtitle str | None

Optional line under the heading explaining the group.

Source code in tempestweb/presets/models.py
@dataclass(frozen=True, slots=True)
class FormSection:
    """A group of related fields under one heading.

    Attributes:
        title: The group heading ("Dados da conta", "Notificações").
        fields: The fields in the group, laid out in the responsive form grid.
        subtitle: Optional line under the heading explaining the group.
    """

    title: str
    fields: list[FormField] = field(default_factory=list)
    subtitle: str | None = None

Kpi dataclass

A single headline number on a dashboard.

Attributes:

Name Type Description
label str

What the number measures ("Receita", "Churn").

value str

The number, already formatted for display ("R$ 82k", "1,8%"). Presets never format numbers: locale and currency are the app's call.

delta str | None

Optional change indicator shown next to the value ("+12%").

up bool

Whether delta is an increase. Drives the arrow direction only — whether up is good is the app's business, expressed via tone.

tone Tone

The delta's semantic colour.

Source code in tempestweb/presets/models.py
@dataclass(frozen=True, slots=True)
class Kpi:
    """A single headline number on a dashboard.

    Attributes:
        label: What the number measures ("Receita", "Churn").
        value: The number, already formatted for display ("R$ 82k", "1,8%").
            Presets never format numbers: locale and currency are the app's call.
        delta: Optional change indicator shown next to the value ("+12%").
        up: Whether ``delta`` is an increase. Drives the arrow direction only —
            whether up is *good* is the app's business, expressed via ``tone``.
        tone: The delta's semantic colour.
    """

    label: str
    value: str
    delta: str | None = None
    up: bool = True
    tone: Tone = "neutral"

NavItem dataclass

One entry in the admin shell's sidebar.

Attributes:

Name Type Description
label str

The text shown in the sidebar.

value str

The value handed to on_navigate when the entry is chosen, and compared against the shell's active to mark the current entry.

badge str | None

Optional short text rendered as a trailing badge ("3", "novo"); None renders no badge.

Source code in tempestweb/presets/models.py
@dataclass(frozen=True, slots=True)
class NavItem:
    """One entry in the admin shell's sidebar.

    Attributes:
        label: The text shown in the sidebar.
        value: The value handed to ``on_navigate`` when the entry is chosen, and
            compared against the shell's ``active`` to mark the current entry.
        badge: Optional short text rendered as a trailing badge (``"3"``,
            ``"novo"``); ``None`` renders no badge.
    """

    label: str
    value: str
    badge: str | None = None

Section dataclass

A titled block of a dashboard, rendered as a card in the section grid.

Attributes:

Name Type Description
title str

The section heading.

body Widget

The section's content — a chart, a table, any widget.

subtitle str | None

Optional supporting line under the heading.

span Span

"auto" lets the section share a row; "full" makes it take the whole row (a wide chart, a table).

Source code in tempestweb/presets/models.py
@dataclass(frozen=True, slots=True)
class Section:
    """A titled block of a dashboard, rendered as a card in the section grid.

    Attributes:
        title: The section heading.
        body: The section's content — a chart, a table, any widget.
        subtitle: Optional supporting line under the heading.
        span: ``"auto"`` lets the section share a row; ``"full"`` makes it take
            the whole row (a wide chart, a table).
    """

    title: str
    body: Widget
    subtitle: str | None = None
    span: Span = "auto"

TableColumn dataclass

One column of a list page's table.

Attributes:

Name Type Description
label str

The header text.

align Align

How the column's cells are aligned. Numbers usually read better as "end".

Source code in tempestweb/presets/models.py
@dataclass(frozen=True, slots=True)
class TableColumn:
    """One column of a list page's table.

    Attributes:
        label: The header text.
        align: How the column's cells are aligned. Numbers usually read better
            as ``"end"``.
    """

    label: str
    align: Align = "start"

auth_page

auth_page(*, title: str, body: Widget, subtitle: str | None = None, brand: str | None = None, footer: Sequence[Widget] = (), key: str = 'tw-auth') -> Widget

Build a sign-in / sign-up screen around an existing form.

The card is centred vertically and horizontally and capped at a readable width, so the same tree is right on a phone and on a 27" monitor without the form stretching across it.

The form itself is yours — LoginForm/SignupForm from :mod:tempestweb.components, or any widget. This preset only places it.

Parameters:

Name Type Description Default
title str

The heading above the form ("Entrar").

required
body Widget

The form widget.

required
subtitle str | None

Optional line under the heading.

None
brand str | None

Optional product name above the heading.

None
footer Sequence[Widget]

Widgets under the card (a "Esqueci minha senha" link, a legal note).

()
key str

The key prefix for the page's widgets.

'tw-auth'

Returns:

Type Description
Widget

The auth page.

Source code in tempestweb/presets/auth.py
def auth_page(
    *,
    title: str,
    body: Widget,
    subtitle: str | None = None,
    brand: str | None = None,
    footer: Sequence[Widget] = (),
    key: str = "tw-auth",
) -> Widget:
    """Build a sign-in / sign-up screen around an existing form.

    The card is centred vertically and horizontally and capped at a readable
    width, so the same tree is right on a phone and on a 27" monitor without the
    form stretching across it.

    The form itself is yours — ``LoginForm``/``SignupForm`` from
    :mod:`tempestweb.components`, or any widget. This preset only places it.

    Args:
        title: The heading above the form ("Entrar").
        body: The form widget.
        subtitle: Optional line under the heading.
        brand: Optional product name above the heading.
        footer: Widgets under the card (a "Esqueci minha senha" link, a legal
            note).
        key: The key prefix for the page's widgets.

    Returns:
        The auth page.
    """
    head: list[Widget] = []
    if brand is not None:
        head.append(muted(brand, key=f"{key}-brand"))
    head.append(heading(title, key=f"{key}-title", level="page"))
    if subtitle is not None:
        head.append(muted(subtitle, key=f"{key}-subtitle"))

    card = Card(
        key=f"{key}-card-inner",
        children=[
            Column(key=f"{key}-head", style=Style(gap=4.0), children=head),
            body,
        ],
    )
    children: list[Widget] = [box(roles.AUTH_CARD, [card], key=f"{key}-card")]
    if footer:
        children.append(
            Column(key=f"{key}-footer", style=Style(gap=8.0), children=list(footer))
        )
    return box(roles.AUTH, children, key=key)

dashboard_page

dashboard_page(*, title: str, kpis: Sequence[Kpi] = (), sections: Sequence[Section] = (), subtitle: str | None = None, actions: Sequence[Widget] = (), key: str = 'tw-dashboard') -> Widget

Build a dashboard: a header, a KPI row and a grid of sections.

Parameters:

Name Type Description Default
title str

The page title.

required
kpis Sequence[Kpi]

Headline numbers shown above the sections. Empty renders no row.

()
sections Sequence[Section]

Titled content blocks. Empty renders no grid.

()
subtitle str | None

Optional line under the title.

None
actions Sequence[Widget]

Buttons shown opposite the title (a period picker, "Exportar").

()
key str

The key prefix for the page's widgets.

'tw-dashboard'

Returns:

Type Description
Widget

The dashboard page.

Source code in tempestweb/presets/dashboard.py
def dashboard_page(
    *,
    title: str,
    kpis: Sequence[Kpi] = (),
    sections: Sequence[Section] = (),
    subtitle: str | None = None,
    actions: Sequence[Widget] = (),
    key: str = "tw-dashboard",
) -> Widget:
    """Build a dashboard: a header, a KPI row and a grid of sections.

    Args:
        title: The page title.
        kpis: Headline numbers shown above the sections. Empty renders no row.
        sections: Titled content blocks. Empty renders no grid.
        subtitle: Optional line under the title.
        actions: Buttons shown opposite the title (a period picker, "Exportar").
        key: The key prefix for the page's widgets.

    Returns:
        The dashboard page.
    """
    children: list[Widget] = [
        page_header(
            title=title, subtitle=subtitle, actions=actions, key=f"{key}-header"
        )
    ]
    if kpis:
        children.append(kpi_grid(kpis, key=f"{key}-kpis"))
    if sections:
        children.append(section_grid(sections, key=f"{key}-sections"))
    return box(roles.PAGE, children, key=key)

kpi_grid

kpi_grid(kpis: Sequence[Kpi], *, key: str = 'tw-kpis') -> Widget

Render headline numbers in a grid that reflows with the viewport.

The grid is auto-fit, so four KPIs become two columns on a tablet and one on a phone with no breakpoint of your own — and adding a fifth KPI does not require touching a layout.

Parameters:

Name Type Description Default
kpis Sequence[Kpi]

The numbers to show, in order.

required
key str

The key prefix.

'tw-kpis'

Returns:

Type Description
Widget

The KPI grid container.

Source code in tempestweb/presets/dashboard.py
def kpi_grid(kpis: Sequence[Kpi], *, key: str = "tw-kpis") -> Widget:
    """Render headline numbers in a grid that reflows with the viewport.

    The grid is ``auto-fit``, so four KPIs become two columns on a tablet and one
    on a phone with no breakpoint of your own — and adding a fifth KPI does not
    require touching a layout.

    Args:
        kpis: The numbers to show, in order.
        key: The key prefix.

    Returns:
        The KPI grid container.
    """
    cards: list[Widget] = [
        StatCard(
            key=f"{key}-{index}",
            label=kpi.label,
            value=kpi.value,
            delta=kpi.delta,
            delta_up=kpi.up,
            color_scheme=_TONE_SCHEME.get(kpi.tone, "surface"),
        )
        for index, kpi in enumerate(kpis)
    ]
    return box(roles.KPI_GRID, cards, key=key)

section_grid

section_grid(sections: Sequence[Section], *, key: str = 'tw-sections') -> Widget

Render titled content blocks as cards in a reflowing grid.

A section with span="full" takes the whole row — what a wide chart or a table wants — while the rest share the available tracks.

Parameters:

Name Type Description Default
sections Sequence[Section]

The sections to render, in order.

required
key str

The key prefix.

'tw-sections'

Returns:

Type Description
Widget

The section grid container.

Source code in tempestweb/presets/dashboard.py
def section_grid(sections: Sequence[Section], *, key: str = "tw-sections") -> Widget:
    """Render titled content blocks as cards in a reflowing grid.

    A section with ``span="full"`` takes the whole row — what a wide chart or a
    table wants — while the rest share the available tracks.

    Args:
        sections: The sections to render, in order.
        key: The key prefix.

    Returns:
        The section grid container.
    """
    blocks: list[Widget] = []
    for index, section in enumerate(sections):
        head: list[Widget] = [
            heading(section.title, key=f"{key}-{index}-title", level="group")
        ]
        if section.subtitle is not None:
            head.append(muted(section.subtitle, key=f"{key}-{index}-subtitle"))
        blocks.append(
            box(
                roles.SECTION,
                [
                    Card(
                        key=f"{key}-{index}-card",
                        children=[
                            Column(
                                key=f"{key}-{index}-head",
                                style=Style(gap=4.0),
                                children=head,
                            ),
                            section.body,
                        ],
                    )
                ],
                key=f"{key}-{index}",
                attrs={"data-tw-span": section.span},
            )
        )
    return box(roles.SECTION_GRID, blocks, key=key)

form_page

form_page(*, title: str, fields: Sequence[FormField] = (), sections: Sequence[FormSection] = (), actions: Sequence[Widget] = (), subtitle: str | None = None, key: str = 'tw-form') -> Widget

Build a form screen: fields in a responsive grid over an action bar.

Pass fields for a flat form or sections for a grouped one; passing both puts the loose fields first, in their own grid. Field widths come from the grid, which fits as many columns as the viewport allows and drops to one on a phone — a field marked span="full" always takes the whole row.

The action bar sits at the end: a right-aligned row on a wide screen, and a stack on a phone. The stack is reversed (column-reverse in client/layouts.js), so the last entry in actions renders on top — put the primary action last and it leads the stack, the way Material stacks dialog buttons.

See :func:settings_page for the grouped-only variant of this same page.

Parameters:

Name Type Description Default
title str

The page title.

required
fields Sequence[FormField]

Ungrouped fields, rendered before any sections.

()
sections Sequence[FormSection]

Grouped fields, each rendered as a card.

()
actions Sequence[Widget]

The submit/cancel buttons. Empty renders no action bar.

()
subtitle str | None

Optional line under the title.

None
key str

The key prefix for the page's widgets.

'tw-form'

Returns:

Type Description
Widget

The form page.

Source code in tempestweb/presets/forms.py
def form_page(
    *,
    title: str,
    fields: Sequence[FormField] = (),
    sections: Sequence[FormSection] = (),
    actions: Sequence[Widget] = (),
    subtitle: str | None = None,
    key: str = "tw-form",
) -> Widget:
    """Build a form screen: fields in a responsive grid over an action bar.

    Pass ``fields`` for a flat form or ``sections`` for a grouped one; passing
    both puts the loose fields first, in their own grid. Field widths come from
    the grid, which fits as many columns as the viewport allows and drops to one
    on a phone — a field marked ``span="full"`` always takes the whole row.

    The action bar sits at the end: a right-aligned row on a wide screen, and a
    stack on a phone. The stack is **reversed** (``column-reverse`` in
    ``client/layouts.js``), so the *last* entry in ``actions`` renders on top —
    put the primary action last and it leads the stack, the way Material stacks
    dialog buttons.

    See :func:`settings_page` for the grouped-only variant of this same page.

    Args:
        title: The page title.
        fields: Ungrouped fields, rendered before any sections.
        sections: Grouped fields, each rendered as a card.
        actions: The submit/cancel buttons. Empty renders no action bar.
        subtitle: Optional line under the title.
        key: The key prefix for the page's widgets.

    Returns:
        The form page.
    """
    children: list[Widget] = [
        page_header(title=title, subtitle=subtitle, key=f"{key}-header")
    ]
    if fields:
        children.append(
            box(
                roles.FORM_GRID,
                [
                    _field(item, key=f"{key}-f{index}")
                    for index, item in enumerate(fields)
                ],
                key=f"{key}-grid",
            )
        )
    for index, section in enumerate(sections):
        children.append(form_section(section, key=f"{key}-s{index}"))
    if actions:
        children.append(box(roles.FORM_ACTIONS, actions, key=f"{key}-actions"))
    return box(roles.PAGE, children, key=key)

form_section

form_section(section: FormSection, *, key: str) -> Widget

Render a titled group of fields as a card.

Parameters:

Name Type Description Default
section FormSection

The group to render.

required
key str

The widget key prefix.

required

Returns:

Type Description
Widget

The section card.

Source code in tempestweb/presets/forms.py
def form_section(section: FormSection, *, key: str) -> Widget:
    """Render a titled group of fields as a card.

    Args:
        section: The group to render.
        key: The widget key prefix.

    Returns:
        The section card.
    """
    head: list[Widget] = [heading(section.title, key=f"{key}-title", level="group")]
    if section.subtitle is not None:
        head.append(muted(section.subtitle, key=f"{key}-subtitle"))
    fields = box(
        roles.FORM_GRID,
        [
            _field(item, key=f"{key}-f{index}")
            for index, item in enumerate(section.fields)
        ],
        key=f"{key}-grid",
    )
    return Card(
        key=key,
        children=[
            Column(key=f"{key}-head", style=Style(gap=4.0), children=head),
            fields,
        ],
    )

settings_page

settings_page(*, title: str, sections: Sequence[FormSection], actions: Sequence[Widget] = (), subtitle: str | None = None, key: str = 'tw-settings') -> Widget

Build a settings screen — a form page whose fields are always grouped.

Renders identically to :func:form_page; this is a deliberate public facade over it, not a distinct layout. Two things differ, and neither is visual:

  • the signature — sections is required and loose fields are not accepted, because a settings screen always groups its fields;
  • the key prefix — tw-settings rather than tw-form, so the two kinds of screen keep distinct widget keys.

Reach for :func:form_page when the screen is a single flat form.

Parameters:

Name Type Description Default
title str

The page title.

required
sections Sequence[FormSection]

The setting groups, each rendered as a card.

required
actions Sequence[Widget]

The save/discard buttons. See :func:form_page for how they stack on a phone.

()
subtitle str | None

Optional line under the title.

None
key str

The key prefix for the page's widgets.

'tw-settings'

Returns:

Type Description
Widget

The settings page.

Source code in tempestweb/presets/forms.py
def settings_page(
    *,
    title: str,
    sections: Sequence[FormSection],
    actions: Sequence[Widget] = (),
    subtitle: str | None = None,
    key: str = "tw-settings",
) -> Widget:
    """Build a settings screen — a form page whose fields are always grouped.

    Renders **identically** to :func:`form_page`; this is a deliberate public
    facade over it, not a distinct layout. Two things differ, and neither is
    visual:

    * the signature — ``sections`` is required and loose ``fields`` are not
      accepted, because a settings screen always groups its fields;
    * the key prefix — ``tw-settings`` rather than ``tw-form``, so the two kinds
      of screen keep distinct widget keys.

    Reach for :func:`form_page` when the screen is a single flat form.

    Args:
        title: The page title.
        sections: The setting groups, each rendered as a card.
        actions: The save/discard buttons. See :func:`form_page` for how they
            stack on a phone.
        subtitle: Optional line under the title.
        key: The key prefix for the page's widgets.

    Returns:
        The settings page.
    """
    return form_page(
        title=title,
        sections=sections,
        actions=actions,
        subtitle=subtitle,
        key=key,
    )

box

box(role: str, children: Sequence[Widget], *, key: str, attrs: dict[str, str] | None = None, style: Style | None = None) -> Widget

Wrap children in a container tagged with a layout role.

Parameters:

Name Type Description Default
role str

A role from :mod:tempestweb.presets.roles; the stylesheet keys its rules off this value.

required
children Sequence[Widget]

The container's children, in order.

required
key str

The widget key, unique among its siblings.

required
attrs dict[str, str] | None

Extra attributes merged after the role (data-tw-open, data-tw-span, …). It cannot override the role itself.

None
style Style | None

Optional inline style. Leave it unset for anything the sheet lays out — an inline declaration wins over the sheet's rule.

None

Returns:

Type Description
Widget

The tagged container.

Source code in tempestweb/presets/layout.py
def box(
    role: str,
    children: Sequence[Widget],
    *,
    key: str,
    attrs: dict[str, str] | None = None,
    style: Style | None = None,
) -> Widget:
    """Wrap ``children`` in a container tagged with a layout role.

    Args:
        role: A role from :mod:`tempestweb.presets.roles`; the stylesheet keys
            its rules off this value.
        children: The container's children, in order.
        key: The widget key, unique among its siblings.
        attrs: Extra attributes merged after the role (``data-tw-open``,
            ``data-tw-span``, …). It cannot override the role itself.
        style: Optional inline style. Leave it unset for anything the sheet lays
            out — an inline declaration wins over the sheet's rule.

    Returns:
        The tagged container.
    """
    merged: dict[str, str] = dict(attrs or {})
    merged[roles.LAYOUT_ATTR] = role
    return Stack(key=key, children=list(children), attrs=merged, style=style)

heading

heading(text: str, *, key: str, level: Level = 'section') -> Widget

Render a page, section or group heading.

Size, weight and colour come from the stylesheet, not from here. A preset that hard-coded them would pick a colour from one palette and land on a page themed with another — a white title on a white page. The sheet resolves them from the theme's own tokens, so a rebranded app rebrands its headings too.

Parameters:

Name Type Description Default
text str

The heading text.

required
key str

The widget key.

required
level Level

Which step of the type scale to use.

'section'

Returns:

Type Description
Widget

The heading text, tagged for the sheet.

Source code in tempestweb/presets/layout.py
def heading(text: str, *, key: str, level: Level = "section") -> Widget:
    """Render a page, section or group heading.

    Size, weight and colour come from the stylesheet, not from here. A preset
    that hard-coded them would pick a colour from one palette and land on a page
    themed with another — a white title on a white page. The sheet resolves them
    from the theme's own tokens, so a rebranded app rebrands its headings too.

    Args:
        text: The heading text.
        key: The widget key.
        level: Which step of the type scale to use.

    Returns:
        The heading text, tagged for the sheet.
    """
    return Text(
        content=text,
        key=key,
        attrs={roles.LAYOUT_ATTR: roles.TITLE, "data-tw-level": level},
    )

muted

muted(text: str, *, key: str) -> Widget

Render supporting text (a subtitle, a hint, a help line).

Parameters:

Name Type Description Default
text str

The text.

required
key str

The widget key.

required

Returns:

Type Description
Widget

The text, tagged so the sheet gives it the muted treatment.

Source code in tempestweb/presets/layout.py
def muted(text: str, *, key: str) -> Widget:
    """Render supporting text (a subtitle, a hint, a help line).

    Args:
        text: The text.
        key: The widget key.

    Returns:
        The text, tagged so the sheet gives it the muted treatment.
    """
    return Text(content=text, key=key, attrs={roles.LAYOUT_ATTR: roles.SUBTITLE})

page_header

page_header(*, title: str, key: str, subtitle: str | None = None, actions: Sequence[Widget] = ()) -> Widget

Render a page's title block with its action buttons.

The title and the actions sit on one row on a wide screen and stack on a phone — the sheet handles the switch, so nothing here measures the viewport.

Parameters:

Name Type Description Default
title str

The page title.

required
key str

The key prefix for the header's widgets.

required
subtitle str | None

Optional line under the title.

None
actions Sequence[Widget]

Buttons shown opposite the title ("Novo", "Exportar").

()

Returns:

Type Description
Widget

The header container.

Source code in tempestweb/presets/layout.py
def page_header(
    *,
    title: str,
    key: str,
    subtitle: str | None = None,
    actions: Sequence[Widget] = (),
) -> Widget:
    """Render a page's title block with its action buttons.

    The title and the actions sit on one row on a wide screen and stack on a
    phone — the sheet handles the switch, so nothing here measures the viewport.

    Args:
        title: The page title.
        key: The key prefix for the header's widgets.
        subtitle: Optional line under the title.
        actions: Buttons shown opposite the title ("Novo", "Exportar").

    Returns:
        The header container.
    """
    titles: list[Widget] = [heading(title, key=f"{key}-title", level="page")]
    if subtitle is not None:
        titles.append(muted(subtitle, key=f"{key}-subtitle"))
    children: list[Widget] = [
        Column(key=f"{key}-titles", style=Style(gap=4.0), children=titles)
    ]
    if actions:
        children.append(box(roles.PAGE_ACTIONS, actions, key=f"{key}-actions"))
    return box(roles.PAGE_HEADER, children, key=key)

data_table

data_table(*, columns: Sequence[TableColumn], rows: Sequence[Sequence[Cell]], key: str = 'tw-table') -> Widget

Render a table that scrolls sideways under a header that stays put.

The table is built here rather than delegated to the core's DataTable because the core resolves row and header backgrounds inline, and inline beats the stylesheet: zebra striping, row hover and a sticky head would all be dead rules. These containers carry no inline background, so client/layouts.js owns the look — and a narrow viewport gets a horizontally scrolling table instead of a squashed one.

Parameters:

Name Type Description Default
columns Sequence[TableColumn]

The column definitions, in order.

required
rows Sequence[Sequence[Cell]]

One sequence of cells per row, each aligned with columns. A cell is a string or a widget.

required
key str

The key prefix.

'tw-table'

Returns:

Type Description
Widget

The scroll container wrapping the table.

Source code in tempestweb/presets/listing.py
def data_table(
    *,
    columns: Sequence[TableColumn],
    rows: Sequence[Sequence[Cell]],
    key: str = "tw-table",
) -> Widget:
    """Render a table that scrolls sideways under a header that stays put.

    The table is built here rather than delegated to the core's ``DataTable``
    because the core resolves row and header backgrounds **inline**, and inline
    beats the stylesheet: zebra striping, row hover and a sticky head would all
    be dead rules. These containers carry no inline background, so
    ``client/layouts.js`` owns the look — and a narrow viewport gets a
    horizontally scrolling table instead of a squashed one.

    Args:
        columns: The column definitions, in order.
        rows: One sequence of cells per row, each aligned with ``columns``. A
            cell is a string or a widget.
        key: The key prefix.

    Returns:
        The scroll container wrapping the table.
    """
    head = box(
        roles.TABLE_HEAD,
        [
            _cell(
                column.label,
                role=roles.TABLE_HEADER_CELL,
                align=column.align,
                key=f"{key}-th-{index}",
            )
            for index, column in enumerate(columns)
        ],
        key=f"{key}-head",
    )
    body: list[Widget] = [head]
    for row_index, row in enumerate(rows):
        body.append(
            box(
                roles.TABLE_ROW,
                [
                    _cell(
                        value,
                        role=roles.TABLE_CELL,
                        align=columns[cell_index].align
                        if cell_index < len(columns)
                        else "start",
                        key=f"{key}-td-{row_index}-{cell_index}",
                    )
                    for cell_index, value in enumerate(row)
                ],
                key=f"{key}-tr-{row_index}",
            )
        )
    table = box(roles.TABLE, body, key=key)
    return box(roles.TABLE_SCROLL, [table], key=f"{key}-scroll")

list_page

list_page(*, title: str, columns: Sequence[TableColumn], rows: Sequence[Sequence[Cell]], subtitle: str | None = None, actions: Sequence[Widget] = (), search: str | None = None, on_search: Callable[[str], None] | None = None, search_placeholder: str = 'Buscar…', filters: Sequence[Widget] = (), page: int = 1, page_count: int = 1, on_page: Callable[[int], None] | None = None, empty_title: str = 'Nada por aqui', empty_subtitle: str | None = None, empty_action: Widget | None = None, key: str = 'tw-list') -> Widget

Build the standard admin listing screen.

Header, a toolbar with search and filters, the table, and pagination. When rows is empty the table is replaced by an empty state — an empty result is a normal outcome, so it gets a designed screen rather than a bare table with no lines.

Parameters:

Name Type Description Default
title str

The page title.

required
columns Sequence[TableColumn]

The table's columns.

required
rows Sequence[Sequence[Cell]]

The rows to show — already filtered and paged by the app. The preset never slices data: what you pass is what is drawn.

required
subtitle str | None

Optional line under the title.

None
actions Sequence[Widget]

Buttons opposite the title ("Novo usuário").

()
search str | None

The current search text. None omits the search box.

None
on_search Callable[[str], None] | None

Called with the new text as the user types. Required for the search box to appear.

None
search_placeholder str

Placeholder for the search box.

'Buscar…'
filters Sequence[Widget]

Extra toolbar widgets (selects, chips, a date range).

()
page int

The current 1-based page.

1
page_count int

The total number of pages. 1 omits the pagination row.

1
on_page Callable[[int], None] | None

Called with the requested page. Required for pagination.

None
empty_title str

Heading of the empty state.

'Nada por aqui'
empty_subtitle str | None

Supporting line of the empty state.

None
empty_action Widget | None

Optional button in the empty state ("Criar o primeiro").

None
key str

The key prefix for the page's widgets.

'tw-list'

Returns:

Type Description
Widget

The list page.

Source code in tempestweb/presets/listing.py
def list_page(
    *,
    title: str,
    columns: Sequence[TableColumn],
    rows: Sequence[Sequence[Cell]],
    subtitle: str | None = None,
    actions: Sequence[Widget] = (),
    search: str | None = None,
    on_search: Callable[[str], None] | None = None,
    search_placeholder: str = "Buscar…",
    filters: Sequence[Widget] = (),
    page: int = 1,
    page_count: int = 1,
    on_page: Callable[[int], None] | None = None,
    empty_title: str = "Nada por aqui",
    empty_subtitle: str | None = None,
    empty_action: Widget | None = None,
    key: str = "tw-list",
) -> Widget:
    """Build the standard admin listing screen.

    Header, a toolbar with search and filters, the table, and pagination. When
    ``rows`` is empty the table is replaced by an empty state — an empty result
    is a normal outcome, so it gets a designed screen rather than a bare table
    with no lines.

    Args:
        title: The page title.
        columns: The table's columns.
        rows: The rows to show — already filtered and paged by the app. The
            preset never slices data: what you pass is what is drawn.
        subtitle: Optional line under the title.
        actions: Buttons opposite the title ("Novo usuário").
        search: The current search text. ``None`` omits the search box.
        on_search: Called with the new text as the user types. Required for the
            search box to appear.
        search_placeholder: Placeholder for the search box.
        filters: Extra toolbar widgets (selects, chips, a date range).
        page: The current 1-based page.
        page_count: The total number of pages. ``1`` omits the pagination row.
        on_page: Called with the requested page. Required for pagination.
        empty_title: Heading of the empty state.
        empty_subtitle: Supporting line of the empty state.
        empty_action: Optional button in the empty state ("Criar o primeiro").
        key: The key prefix for the page's widgets.

    Returns:
        The list page.
    """
    children: list[Widget] = [
        page_header(
            title=title, subtitle=subtitle, actions=actions, key=f"{key}-header"
        )
    ]

    toolbar_items: list[Widget] = []
    if search is not None and on_search is not None:
        forward = _search_adapter(on_search)
        toolbar_items.append(
            SearchBar(
                value=search,
                placeholder=search_placeholder,
                on_change=forward,
                key=f"{key}-search",
            )
        )
    if filters:
        toolbar_items.append(
            Row(key=f"{key}-filters", style=Style(gap=8.0), children=list(filters))
        )
    if toolbar_items:
        children.append(box(roles.TOOLBAR, toolbar_items, key=f"{key}-toolbar"))

    if rows:
        children.append(data_table(columns=columns, rows=rows, key=f"{key}-table"))
        if page_count > 1 and on_page is not None:
            children.append(
                _pagination(
                    page=page,
                    page_count=page_count,
                    on_page=on_page,
                    key=f"{key}-pagination",
                )
            )
    else:
        children.append(
            EmptyState(
                title=empty_title,
                subtitle=empty_subtitle,
                action=empty_action,
                key=f"{key}-empty",
            )
        )
    return box(roles.PAGE, children, key=key)

admin_shell

admin_shell(*, title: str, nav: Sequence[NavItem], active: str, on_navigate: Callable[[str], None], body: Widget, brand: str | None = None, actions: Sequence[Widget] = (), footer: Widget | None = None, sidebar_open: bool = False, on_toggle_sidebar: Callable[[], None] | None = None, key: str = 'tw-shell') -> Widget

Build an admin shell around body.

The layout is a grid: the sidebar owns a column and the header a row, so the content area never needs a margin kept in sync with the sidebar's width. Below 1024px the sidebar leaves the grid and becomes an overlay — pass sidebar_open and on_toggle_sidebar to drive it, and the burger button appears (the sheet hides it on wide screens, where the sidebar is permanent).

Nothing here measures the viewport: every breakpoint lives in client/layouts.js. The same tree is correct at any width.

Parameters:

Name Type Description Default
title str

The application title, shown in the header.

required
nav Sequence[NavItem]

The sidebar entries.

required
active str

The value of the current entry, matched against nav.

required
on_navigate Callable[[str], None]

Called with an entry's value when it is chosen.

required
body Widget

The content area — usually a page preset.

required
brand str | None

Optional product name shown above the nav.

None
actions Sequence[Widget]

Widgets pinned to the right of the header (a user menu, a "Sair" button).

()
footer Widget | None

Optional widget pinned under the nav (the signed-in user).

None
sidebar_open bool

Whether the overlay sidebar is open. Ignored on wide screens, where the sidebar is always visible.

False
on_toggle_sidebar Callable[[], None] | None

Called when the burger or the scrim is tapped. Pass None when the app has no small-screen story and the burger is omitted.

None
key str

The key prefix for the shell's widgets.

'tw-shell'

Returns:

Type Description
Widget

The shell widget tree.

Source code in tempestweb/presets/shell.py
def admin_shell(
    *,
    title: str,
    nav: Sequence[NavItem],
    active: str,
    on_navigate: Callable[[str], None],
    body: Widget,
    brand: str | None = None,
    actions: Sequence[Widget] = (),
    footer: Widget | None = None,
    sidebar_open: bool = False,
    on_toggle_sidebar: Callable[[], None] | None = None,
    key: str = "tw-shell",
) -> Widget:
    """Build an admin shell around ``body``.

    The layout is a grid: the sidebar owns a column and the header a row, so the
    content area never needs a margin kept in sync with the sidebar's width.
    Below 1024px the sidebar leaves the grid and becomes an overlay — pass
    ``sidebar_open`` and ``on_toggle_sidebar`` to drive it, and the burger button
    appears (the sheet hides it on wide screens, where the sidebar is permanent).

    Nothing here measures the viewport: every breakpoint lives in
    ``client/layouts.js``. The same tree is correct at any width.

    Args:
        title: The application title, shown in the header.
        nav: The sidebar entries.
        active: The ``value`` of the current entry, matched against ``nav``.
        on_navigate: Called with an entry's ``value`` when it is chosen.
        body: The content area — usually a page preset.
        brand: Optional product name shown above the nav.
        actions: Widgets pinned to the right of the header (a user menu, a
            "Sair" button).
        footer: Optional widget pinned under the nav (the signed-in user).
        sidebar_open: Whether the overlay sidebar is open. Ignored on wide
            screens, where the sidebar is always visible.
        on_toggle_sidebar: Called when the burger or the scrim is tapped. Pass
            ``None`` when the app has no small-screen story and the burger is
            omitted.
        key: The key prefix for the shell's widgets.

    Returns:
        The shell widget tree.
    """
    open_flag = "true" if sidebar_open else "false"

    nav_children: list[Widget] = []
    if brand is not None:
        nav_children.append(
            Column(
                key=f"{key}-brand",
                style=Style(padding=Edge.all(16.0)),
                children=[muted(brand, key=f"{key}-brand-text")],
            )
        )
    nav_children.append(
        Column(
            key=f"{key}-nav",
            style=Style(gap=4.0, padding=Edge.symmetric(vertical=8.0, horizontal=8.0)),
            children=[
                _nav_button(item, active=item.value == active, on_navigate=on_navigate)
                for item in nav
            ],
        )
    )
    if footer is not None:
        nav_children.append(
            Column(
                key=f"{key}-footer",
                style=Style(padding=Edge.all(16.0)),
                children=[footer],
            )
        )

    header_actions: list[Widget] = list(actions)
    leading: Widget | None = None
    if on_toggle_sidebar is not None:
        leading = Button(
            label="☰",
            on_click=on_toggle_sidebar,
            key=f"{key}-burger",
            semantics=Semantics(label="Abrir menu"),
            attrs={roles.LAYOUT_ATTR: roles.SHELL_BURGER},
            style=Style(
                padding=Edge.symmetric(vertical=8.0, horizontal=12.0),
                radius=8.0,
                background=ACCENT,
                color=_ON_ACCENT,
            ),
        )

    children: list[Widget] = [
        box(
            roles.SHELL_SIDEBAR,
            nav_children,
            key=f"{key}-sidebar",
            attrs={"data-tw-open": open_flag},
            style=Style(background=SURFACE),
        ),
        box(
            roles.SHELL_HEADER,
            [
                AppBar(
                    title=title,
                    leading=leading,
                    actions=header_actions,
                    key=f"{key}-appbar",
                )
            ],
            key=f"{key}-header",
        ),
        box(roles.SHELL_MAIN, [body], key=f"{key}-main"),
    ]
    if on_toggle_sidebar is not None:
        children.append(
            _scrim(key=f"{key}-scrim", open_flag=open_flag, on_close=on_toggle_sidebar)
        )
    return box(roles.SHELL, children, key=key)