Skip to main content

Page registration

Capsule supports two page models, and most real apps use both. The usual split is simple. Use the Python DSL for dashboards, metrics, settings, tables, and task views. Use React when the page starts to feel like a richer application surface, such as a mailbox, approval queue, or list-detail workflow. Both page types can be public or gated to signed-in users of the deployed app with access="authenticated".

Worked example

A strong Capsule app often uses both page models at once:
@app.page("Overview", icon="layout-dashboard", access="authenticated")
def overview():
    return cpsl.ui.Page(
        [
            cpsl.ui.Row(
                [
                    cpsl.ui.Metric("Threads", data="mailbox_metrics", field="total_threads"),
                    cpsl.ui.Metric("Needs Review", data="mailbox_metrics", field="needs_review"),
                ]
            ),
            cpsl.ui.Card(
                "Mailbox Settings",
                [
                    cpsl.ui.Toggle("Show closed threads", setting="show_closed_threads"),
                    cpsl.ui.Select("Default mailbox tab", setting="default_tab"),
                ],
            ),
        ]
    )


app.add_page(
    "Mailbox",
    icon="mail",
    component="pages/mailbox.tsx",
    access="authenticated",
)
That is the usual split:
  • use the DSL for dashboards, metrics, settings, and task views
  • use React for richer list/detail or workflow-heavy surfaces

Python DSL pages

@app.page("Overview", icon="layout-dashboard", access="authenticated")
def overview():
    return cpsl.ui.Page([...])
Arguments:
ArgumentMeaning
nameSidebar label
iconIcon name
orderExplicit page ordering
access"public" for open pages, or "authenticated" for signed-in users of this app
Example:
@app.page("Overview", icon="layout-dashboard")
def overview():
    return cpsl.ui.Page(
        [
            cpsl.ui.Row(
                [
                    cpsl.ui.Metric("Threads", data="mailbox_metrics", field="total_threads"),
                    cpsl.ui.Metric("Needs Review", data="mailbox_metrics", field="needs_review"),
                ]
            ),
            cpsl.ui.Card(
                "Mailbox Settings",
                [
                    cpsl.ui.Toggle("Show closed threads", setting="show_closed_threads"),
                    cpsl.ui.Select("Default mailbox tab", setting="default_tab"),
                ],
            ),
        ]
    )

React pages

app.add_page(
    "Dashboard",
    icon="chart-column",
    component="pages/dashboard.tsx",
    packages=["lightweight-charts@4.2.0"],
    access="authenticated",
)
Arguments:
ArgumentMeaning
nameSidebar label
iconIcon name
componentPath to the TSX component
packagesExtra npm packages for the page bundle
orderExplicit page ordering
access"public" for open pages, or "authenticated" for signed-in users of this app
Example:
app.add_page(
    "Mailbox",
    icon="mail",
    component="pages/mailbox.tsx",
    access="authenticated",
)

DSL widgets

The Python DSL is deliberately small. You compose a page from a few primitives rather than learning a big UI framework.

Layout

WidgetPurpose
cpsl.ui.Page(children)Root page container
cpsl.ui.Row(children)Horizontal grouping
cpsl.ui.Column(children)Vertical grouping
cpsl.ui.Card(title=None, children=None)Card wrapper
cpsl.ui.Divider()Visual separator
cpsl.ui.Text(content, style=None)Text element

Data display

WidgetPurpose
Metric(...)KPI or summary stat
Table(...)Table from collection, data source, or inline rows
Chart(...)Chart from a data source

Settings widgets

WidgetPurpose
Toggle(label, setting=...)Boolean setting
TextInput(label, setting=..., multiline=False, placeholder=None)String setting
NumberInput(label, setting=..., min=None, max=None, step=None)Numeric setting
Select(label, setting=...)Setting with predefined options

Task UI

WidgetPurpose
TaskBoard(...)Kanban-style task board
TaskBoardColumn(label, statuses)Custom task-board column group
Here is a compact DSL page that uses several of these widgets together:
@app.page("Dashboard", icon="layout-dashboard")
def dashboard():
    return cpsl.ui.Page(
        [
            cpsl.ui.Row(
                [
                    cpsl.ui.Metric("Threads", data="mailbox_metrics", field="total_threads"),
                    cpsl.ui.Metric("Needs Review", data="mailbox_metrics", field="needs_review"),
                ]
            ),
            cpsl.ui.Divider(),
            cpsl.ui.Toggle("Show closed threads", setting="show_closed_threads"),
        ]
    )

Widget notes

  • Metric can display a literal value or bind to data plus field.
  • Table can bind to a collection, a data handler, or inline rows.
  • Chart binds to a named data source and supports line, bar, pie, scatter, and area.
  • Passing a CollectionRef to Table(...) lets Capsule inherit collection metadata automatically.

React page runtime

React pages are built with the @capsule/page runtime. The most common helpers are:
  • useData(...)
  • useCollection(...)
  • useTheme()
  • useCapsule() for the current app user plus login() / logout()
Common import pattern:
import { useCapsule, useCollection, useData, useTheme } from "@capsule/page"
Example:
import { EmptyState, useCapsule, useData, useTheme } from "@capsule/page"

export default function Mailbox() {
  const theme = useTheme()
  const { user } = useCapsule()
  const threads = useData("threads", { params: { status: "needs-review" } })

  if (threads.data?.gmail_connected === false) {
    return (
      <EmptyState
        title="Gmail not connected"
        description="This signed-in app user still needs a Gmail integration before the mailbox can load."
      />
    )
  }

  return (
    <div style={{ color: theme.color.foreground }}>
      <p>Signed in as {user?.email}</p>
      <h2>Threads: {threads.data?.threads?.length ?? 0}</h2>
    </div>
  )
}
For access="authenticated" pages, Capsule handles the sign-in gate before the React page loads. Inside the page, useCapsule().user is the signed-in user for this app. If you want a public page to prompt for sign-in, useCapsule().login() opens the app sign-in flow. React pages are the right choice when you need custom interactivity or external packages. The Python DSL is usually faster when metrics, tables, charts, and settings widgets are enough.

Naming pitfall

Do not confuse:
  • cpsl.Column for collection schema
  • cpsl.ui.Column for page layout

See also