{"version":"v1.0","client_state":{"DocsNavigation.default":{"fields":{"query":""},"persist":"memory","send_to_server":true},"ColorScheme.default":{"fields":{"mode":"system"},"persist":"local","send_to_server":true},"Filtre.default":{"fields":{"cherche":""},"persist":"memory","send_to_server":true}},"endpoints":{"action":"/_bretzel/action","sse":"/_bretzel/sse","refetch":"/_bretzel/refetch"},"csrf":"37b396b2900e0a57d31c8611b3cd8ab7","page_id":"0aee3c187e7b4b348f99dc8e0964099f","address":""}

What Bretzel can do

23 capabilities, each with the symbol you enter it through and the smallest code that puts it to work. This is the answer to “what can this framework do” — the one neither the folder tree nor the component catalogue can give.
An index lists, a chapter teaches
This page gives an overview. Every capability links to the chapter that explains it, with its entry point in the API and its current limits.
Search by need
Use the filter to search for a need (“csv”, “realtime”, “theme”) even if you do not yet know the name of the matching component.

Serve a file

Return a route that DOWNLOADS instead of navigating. A list of dicts becomes a CSV with its header, its escaping and its BOM for Excel; one touches neither the HTTP headers nor the `Content-Disposition`.
Entry pointsbretzel.downloadbretzel.ui.link
The chapter that teaches it:/browser
@download("/customers.csv")
async def customers_csv() -> list[dict]:
    return [{"name": "Ada Lovelace", "city": "London"}]

# The link must say it carries a FILE, otherwise the shell
# swallows it: `hx-boost` intercepts every <a> and swaps the
# CSV into the page.
ui.link("Export", href="/customers.csv", download=True)
What to know first
Without `download=True`, the link is intercepted by `hx-boost` and the file arrives as HTML in the outlet — with no sign at all on the server side.

Act in the browser without writing JS

Copy to the clipboard, open the share sheet, vibrate the device, print, go full screen — each in one `on_click=`, with no server round trip and not a line of JavaScript.
Entry pointsbretzel.copybretzel.sharebretzel.vibratebretzel.print_pagebretzel.fullscreen
The chapter that teaches it:/browser
ui.button("Copy the key", on_click=copy(state.api_key))
ui.button("Share", on_click=share())
ui.button("Vibrate", on_click=vibrate([50, 30, 50]))
ui.button("Print", on_click=print_page())
ui.button("Full screen", on_click=fullscreen())
What to know first
`share` does not exist on a desktop browser: it falls back on copying the URL. `vibrate` does nothing on a device with no motor. Both are silent by design — a missing verb must not break the page.

Install as an application

Serve a web manifest so the app can be added to the home screen and open with no address bar. One line in `Bretzel(...)`; the manifest, its route and its tags are generated.
Entry pointsbretzel.PWAbretzel.PWAIcon
The chapter that teaches it:/browser
app = Bretzel(
    title="Tracker",
    pwa=PWA(name="Tracker", icon="/static/logo.png",
            theme_color="#0f172a"),
)
What to know first
The manifest is not enough for offline mode: there is no service worker yet, so the installed app still requires the network.

Sign in with a Google, Microsoft or GitHub account

Mount an OAuth2/OIDC door — its route, its token exchange, its signed cookie and its anti-fixation rotation. What is left to write is the only decision that is yours: accept this profile, or not.
Entry pointsbretzel.server.oauth.OIDCbretzel.auth
The chapter that teaches it:/auth
@auth.door(OIDC(
    name="google", issuer="https://accounts.google.com",
    client_id=CLIENT_ID, client_secret=CLIENT_SECRET,
))
def on_oauth_user(profile) -> str | None:
    """Returning None REFUSES entry."""
    if not profile.email.endswith("@mycompany.com"):
        return None
    return profile.email
What to know first
Bretzel owns the identity and its transport; the app owns the proof. There is no sign-in page supplied, no passwords, no roles — and Apple is not handled (its `client_secret` is an ES256 JWT, hence a dependency).

Keep the app's state in typed classes

Declare where a piece of data lives — the page, the session, the user, the whole app, or the browser — by choosing the class one inherits from. No strings to address state, so autocompletion and the type checker work.
Entry pointsbretzel.state.PageStatebretzel.state.SessionStatebretzel.state.UserStatebretzel.state.AppStatebretzel.state.ClientState
The chapter that teaches it:/state-server
class Filter(PageState):        # dies with the page
    search: str = field(default="")

class Cart(SessionState):       # follows the tab
    lines: list[str] = field(default_factory=list)

class Profile(UserState):       # follows the signed-in person
    first_name: str = field(default="")

class Settings(AppState):       # shared by everybody
    maintenance: bool = field(default=False)

class Open(ClientState):        # NEVER leaves the browser
    panel: bool = field(default=False)
What to know first
`UserState` only exists when an identity is resolved: with no auth, an app reading it gets a 401. `ClientState` does not travel as JSON — htmx sends it field by field, which surprises on lists.

Refresh part of a page on a state mutation

Mark a region as depending on a typed state: any mutation of that state re-renders it, alone, without the rest of the page moving and without writing a single network call. With `broadcast=`, the same region re-renders on ALL open pages, over SSE.
Entry pointsbretzel.refreshable
The chapter that teaches it:/reactivity-server
class Cart(SessionState):
    lines: list[str] = field(default_factory=list)

@refreshable(deps=[Cart])
def summary() -> None:
    ui.text(f"{len(Cart().lines)} item(s)")

# Elsewhere, in a handler: the region re-renders by itself.
Cart().lines.append("coffee")
What to know first
The region is TRANSPARENT to layout but is not an instance of what it carries — a container inspecting its children must unwrap it. And `broadcast=` is not a separate capability: it is a mode of that same decorator.

Compute on the client without writing JS

Write a Python expression over client state — comparison, arithmetic, negation, concatenation — and let it evaluate IN the browser. No round trip, and the JS emitted is never typed by hand.
Entry pointsbretzel.state.ClientBinding
The chapter that teaches it:/reactivity-client
class Form(ClientState):
    name: str = field(default="")
    age: int = field(default=0)

f = Form()
# ⚠️ One never WRITES the type's name: it is the operator on
# a client-state field that builds it.
empty: ClientBinding = f.name == ""

# Each of these three lines produces JS, not a POST:
ui.button("Submit", disabled=empty)
ui.text("Adult", visible=f.age >= 18)
ui.text(f.name + " — " + f.name)
What to know first
One never writes `ClientBinding`: it is the TYPE an operator on a client-state field produces. The anchoring gate in fact refused this snippet's first version, which named it nowhere. The algebra is finite: `bretzel describe ClientBinding` lists the operations with the JS each one emits. An expression outside the algebra (a Python function call) evaluates at RENDER time and freezes.

Make a page live without anybody clicking

A counter, a clock, a periodic poll: a client-side cadence that fires a handler, stoppable by a plain state boolean — with no server task and no lifecycle to manage.
Entry pointsbretzel.ui.interval
The chapter that teaches it:/cadence
class Live(ClientState):
    active: bool = field(default=True)

live = Live()
ui.interval(on_tick=reload_rows, seconds=5, active=live.active)
ui.switch("Follow live", value=live.active)
What to know first
This is PULL: the page asks. For PUSH — the server telling it — use `@refreshable(broadcast=[…])`, which goes over SSE and wakes only the pages concerned.

Render a moving list without re-rendering all of it

Iterate with a stable key per item, filter client-side on an expression, paginate — all of it while keeping each row's client state (a ticked box stays ticked when the list reorders).
Entry pointsbretzel.ui.eachbretzel.ui.filter_eachbretzel.ui.paginate_each
The chapter that teaches it:/lists
for task in each(Tasks().items, key=lambda t: t.id):
    ui.checkbox(task.title)

# Filter without going back to the server:
with filter_each(Tasks().items, where=lambda t: t.open):
    ui.text("…")

with paginate_each(Tasks().items, per_page=20):
    ui.text("…")
What to know first
A bare `for` works as long as the body produces no client state: without a stable key, idiomorph re-pairs the nodes by position and the state jumps from one row to another.

Show a table that sorts, filters, paginates and exports

A component that takes a typed query state and some columns, and renders the search box, the clickable headers, the pagination and the CSV export button. Sorting and pagination are done on the SERVER: the table works on a million rows.
Entry pointsbretzel.ui.datatablebretzel.ui.column
The chapter that teaches it:/lists
class Query(DatatableState):
    pass

COLUMNS = [
    column("title", "Title", sortable=True),
    column("status", "Status", align="center"),
]

def load(q):
    """Returns (rows_of_this_page, total)."""
    return page_of(q), total_of(q)

datatable(state=Query, columns=COLUMNS, rows=load,
          exportable=True, export_filename="issues.csv")
What to know first
`rows=` accepts a list (everything in memory) or a callable receiving the query — only the second form avoids loading the whole table.

Move items with the mouse or a finger

Reorder a list, drag a card from one column to another: one declares the zone that accepts and the item that is grabbed, and the handler receives what came from where and where it goes.
Entry pointsbretzel.ui.draggablebretzel.ui.dropzone
The chapter that teaches it:/drag
with dropzone(name="todo", accepts=["task"], on_move=move_task):
    for t in Tasks().todo:
        with draggable(key=t.id, group="task"):
            ui.card(t.title)
What to know first
`accepts=` and `group=` are what keep a card from falling into a zone that does not understand it — without them, every zone accepts everything.

Open and close a surface from the code

Dialogs, drawers, popovers and menus open through a call — `.open()` / `.close()` — instead of a state boolean to wire. It is the default for everything purely visual: nothing to declare, nothing to synchronise.
Entry pointsbretzel.ui.dialogbretzel.ui.drawerbretzel.ui.popoverbretzel.ui.dropdown
The chapter that teaches it:/actions-client
with dialog(title="Confirm") as confirmation:
    ui.text("Delete permanently?")

ui.button("Delete", on_click=confirmation.open())

with drawer(side="right") as panel:
    ui.text("Filters")
with popover() as details:
    ui.text("Detail")
with dropdown() as menu:
    ui.menu_item("Rename")
What to know first
The imperative form is the default for VISUAL surfaces. A component carrying a VALUE (a field, a choice) stays bound through `ClientBinding` — the state is then the truth, not the call.

Freeze the document and scroll the regions

The tool model: the frame does not move, only the columns scroll — a fixed sidebar, a fixed header, content that scrolls on its own. The alternative stays the default: a document that scrolls as a whole.
Entry pointsbretzel.ui.viewportbretzel.ui.pane
The chapter that teaches it:/scrolling
with viewport(direction="row"):
    with pane(padding="md"):
        ui.text("The left column scrolls on its own.")
    with pane(padding="md"):
        ui.text("So does the right one, independently.")
What to know first
Both models coexist in the repository (8 apps against 10) and that is intended. Mixing the two on one page gives two nested scrollbars.

Draw charts without a JS library

Lines, bars, pies, scatter plots and sparklines, rendered as SVG on the server — so visible before a single script runs, and printable.
Entry pointsbretzel.ui.line_chartbretzel.ui.bar_chartbretzel.ui.pie_chartbretzel.ui.scatter_chartbretzel.ui.sparkline
The chapter that teaches it:/charts
line_chart(series=[sales], x_labels=MONTHS, height=240)
bar_chart(series=[by_region])
pie_chart(values=[40, 35, 25], labels=["A", "B", "C"])
scatter_chart(series=[cloud])
sparkline(values=[3, 5, 4, 8, 6])
What to know first
This is static SVG enriched on the client: no zoom, no panning, no millions of points. For interactive exploration, a dedicated library remains the right tool.

Change the whole visual identity from a palette

One semantic colour is enough: light, dark, hover states and contrasts are derived from it, and every component follows. Tailwind v4 is compiled by a Rust binary — no Node.js in production.
Entry pointsbretzel.theme.Themebretzel.theme.Palette
The chapter that teaches it:/theme
THEME = Theme(semantic={"primary": "#0f766e"})

# Override ONE component, without touching the others:
THEME = Theme(
    semantic={"primary": "#0f766e"},
    components={"card": {"slots": {"root": "rounded-none"}}},
)
palette = Palette  # what the theme resolves, light AND dark
What to know first
A Tailwind class ASSEMBLED in an f-string is invisible to the production compiler: the HTML is identical on both sides, so the breakage only shows IN production. Whole class in the theme, or safelist.

Split an app into declared features

Each feature declares its name, its nature and what it provides; the graph is checked at startup — an unknown dependency, a cycle, a name collision are errors, not runtime surprises.
Entry pointsbretzel.Feature
The chapter that teaches it:/structure
feature = Feature(
    name="access",
    kind="logic",
    provides=[current_user, sign_out],
)
What to know first
The discovery mechanics (manifest, `include` versus sweeping) are deliberately DEFERRED: `app.include(module)` remains the way to mount a feature.

Speak the visitor's language

The words the framework renders itself — "Search…", "Dismiss alert", the pagination labels — are translated per request, negotiated from the browser's header and overridable by a cookie.
Entry pointsbretzel.render.Languagebretzel.render.text
The chapter that teaches it:/languages
app = Bretzel(
    lang="fr", languages=["fr", "en"],
    texts={"fr": {"alert.dismiss": "Fermer"}},
)

# In a page, read the language resolved for THIS request:
ui.text(f"language = {Language().code}")
# And reuse the framework's vocabulary:
ui.text(text("alert.dismiss"))
What to know first
This covers THE FRAMEWORK's vocabulary. The app's own sentences stay its responsibility — there is no application internationalisation system (planned for 2.1).

Ask the framework what it exposes

Query the INSTALLED code: a component's card with its parameters, its slots and its events; the package tree; a module's surface classified by need. Nothing is copied, so nothing can drift.
Entry pointsbretzel.introspect.describebretzel.introspect.index
The chapter that teaches it:/tree
# On the command line:
#   py -m bretzel.cli.main describe hstack
#   py -m bretzel.cli.main describe capabilities
print(describe("hstack"))
print(index())          # the whole ui.* surface, one line per symbol
What to know first
The output is UTF-8, arrows and quotation marks included: on a Windows console in cp1252, a bare `print` raises. The CLI takes care of it, a home-made script must too.

Have the framework judge your own code

Ten rules that catch what no test sees: the dead kwarg (it does not raise, does not show, is not visible in review), sibling fields at different sizes, a cast that erases a value's provenance and breaks resynchronisation.
Entry pointsbretzel.lint.runbretzel.lint.available_rules
The chapter that teaches it:/structure
# On the command line:
#   py -m bretzel.cli.main check examples
report = run(["examples"])
print(report.exit_code, len(report.findings))

# What the tool can verify, enumerable by design:
for rule in available_rules():
    print(rule)
What to know first
These are RULES, not a reflection of the code — it is the detachable half of the framework, and the `lint-stays-extractable` contract in `.importlinter` keeps it detachable.

Validate a piece of data where it lives

Attach a rule to a typed state FIELD rather than to a form. It runs on every assignment — so from the form as well as from an import or a handler called elsewhere.
Entry pointsbretzel.state.validatorbretzel.ui.formbretzel.ui.form_field
The chapter that teaches it:/forms
class SignUp(SessionState):
    email: str = field(default="")

    @validator("email")
    def _check(self, value: str) -> str:
        value = value.strip().lower()
        if "@" not in value:
            raise ValueError("Invalid address.")
        return value           # it also NORMALISES

with ui.form(on_submit=save):
    with ui.form_field(label="Email", error=error):
        ui.input(name="email")
What to know first
A rule set on the VIEW only covers the path that goes through the view. An app has several write paths and one form — that is why the validator lives on the field, not on the `ui.form`.

Keep state across a restart

Plug in a shared store — Redis — so that the session and user scopes survive the process restarting and are shared between several instances. One URL, and nothing else to change in the code.
Entry pointsbretzel.BretzelConfig
The chapter that teaches it:/config
# Without Redis, state lives in memory and dies with the
# process. Perfect in dev, wrong as soon as there are two
# instances behind a load balancer.
app = Bretzel(
    secret_key="…",
    redis_url="redis://localhost:6379/0",
    session_max_age_days=30,
)

# `BretzelConfig` carries the settings' complete contract.
print(BretzelConfig.__doc__)
What to know first
It is not a database: screen state is not your business data. A cart one must find again in six months goes in a database, not in a `SessionState`.

Refuse a double click and a replay

Mark an action so that a second submission of the same render does not run it twice. The signature already carries a signed timestamp — a late replay is refused by the base layer, with nothing to write.
Entry pointsbretzel.idempotent
The chapter that teaches it:/actions-server
from bretzel import idempotent

@idempotent
def pay() -> None:
    """A double click does not charge twice."""
    charge(Cart().total)
What to know first
The anti-replay shipped is a SIGNED timestamp plus this decorator, not a table of single-use tokens: the signature is computed at RENDER time, so it cannot cover the request's body. Replaying escalates no privilege — the double click is the real subject.

Develop with no build and no manual reload

One word switches the whole toolchain: server reload on the slightest file touched, CSS compiled in the browser, full tracebacks. In production the CSS is compiled ahead of time by a binary, and there is no Node.js at all.
Entry pointsbretzel.Bretzel
The chapter that teaches it:/config
app = Bretzel(
    secret_key="…",
    mode="dev",            # or "prod"
)

if __name__ == "__main__":
    app.run(port=8000, reload=True)
What to know first
Reloading launches uvicorn in a CHILD process. A parent killed without propagating leaves that child holding the port, and the next launch can no longer bind to it — measured, and the error does not always reach the screen.
No capability matches that search.

The same thing on the command line

py -m bretzel.cli.main describe capabilities
For the detail of a symbol named above:ui.* catalogue →The framework's tree →