Components¶
A component is a small, reusable piece of content you put in a
Field — or paint on its own with render() / Terminal. A plain string is
enough for a label. Reach for a component when the slot needs structure,
interaction, or its own live attributes.
What makes components useful is how many patterns stack on the same idea:
nest styled pieces, declare schema on a subclass, keep attributes live, or
subclass Component for something entirely yours.
In a field¶
Use default_factory when the default is a component instance (not a plain
immutable value):
from xnano import BaseGrid, Field
from xnano.components.loader import Loader
from xnano.components.options import Options
class Panel(BaseGrid, direction="vertical", gap=1):
status: str = Field(default="Working…", height=1) # (1)!
progress: Loader = Field(
default_factory=lambda: Loader(value=0.4, style="bar"),
height=1,
)
items: Options = Field(default_factory=Options, border="rounded")
- A
strfield still paints as text — no component required.
Field chrome (border, title, padding, group, …) still applies around
whatever the component paints. See Fields and State.
Interactive
This code block runs in the browser via Pyodide.
Try editing the code!
- Change
value=on theLoader(0.0–1.0). - Change
styleto"line"or"spinner".
from xnano import BaseGrid, Field, render
from xnano.components.loader import Loader
class Panel(BaseGrid, direction="vertical", gap=1):
status: str = Field(default="Working…", height=1)
progress: Loader = Field(
default_factory=lambda: Loader(value=0.4, style="bar"),
height=1,
)
render(Panel())
Nested pieces¶
Several components accept nested content. Text is the usual example: a list
of Text (or strings) becomes independently styled runs in one line.
from xnano.components.text import Text
Text([
Text("● ", foreground="emerald-400"),
Text("healthy ", modifiers=("bold",)),
Text("12ms", foreground="slate-300"),
])
Interactive
This code block runs in the browser via Pyodide.
Try editing the code!
- Change a nested
foreground. - Change the status string.
from xnano import render
from xnano.components.text import Text
render(Text([
Text("● ", foreground="emerald-400"),
Text("healthy ", foreground="white", modifiers=("bold",)),
Text("12ms", foreground="slate-300"),
], background="slate-900"))
The same idea shows up elsewhere: a Button label can be a string or Text;
table cells format through Column callables; charts declare multiple series
on one class.
Live attributes¶
Component fields are live. Change items, value, data, label, … and
the next frame reflects it — same spirit as assigning a grid field.
from xnano import BaseGrid, Context, Field
from xnano.components.options import Options
class SavedNotes(BaseGrid):
notes: Options = Field(default_factory=Options, border="rounded")
def grid_render(self, ctx: Context) -> None:
self.notes.items = tuple(
n.name for n in ctx.state.saved
) or ("no notes yet",)
That is the usual loop: keep shared data in state, push a view of it onto component attributes each frame (or in a hook).
Declarative subclasses¶
Some built-ins are meant to be subclassed with descriptors on the class body. The metaclass collects those descriptors; you get a typed schema without building columns or series by hand every time.
Tables — Column¶
from xnano.components.schema import Column
from xnano.components.table import Table
class Services(Table):
service: str = Column(
header="SERVICE",
accessor=lambda row: row["meta"]["name"],
format=lambda value: value.upper(),
width=14,
)
status: str = Column(
foreground=lambda value: "green-300" if value == "ok" else "red-300",
horizontal_align="center",
)
latency: int = Column(format="{} ms", horizontal_align="right", width=12)
table = Services(data=[
{"meta": {"name": "api"}, "status": "ok", "latency": 12},
{"meta": {"name": "database"}, "status": "degraded", "latency": 340},
])
Without descriptors, Table(data=[...]) still works and infers columns from
keys.
Charts — Series¶
from xnano.components.chart import Chart
from xnano.components.schema import Series
class Latency(Chart):
p50 = Series(color="green", kind="line")
p99 = Series(color="red", kind="scatter", label="99th")
chart = Latency(series={"p50": [1, 2, 3], "p99": [4, 5, 6]})
Descriptors are the “declare once, reuse the type” pattern. Instance
construction stays light: pass data= / series= and go.
Custom components¶
Subclass Component
(prefer @dataclasses.dataclass) when a built-in is not enough.
Patterns available on every custom component:
| Pattern | What you get |
|---|---|
| Dataclass attributes | Live state on the instance |
component_post_init |
Setup after fields are assigned (prefer this over __post_init__) |
| Paint method | Return interface-neutral content for the slot |
visible / z / fit_content |
Shared layout flags on every component |
focused |
Read-only; true while this component holds field focus |
handle_keyboard / handle_paste |
Optional input while focused (True = consumed) |
| Responsive paint variants | Optional size-tier overrides when the viewport changes |
get_size / frame hooks |
Preferred size and before/after paint hooks |
Minimal example:
import dataclasses
from xnano.components.component import Component
from xnano.core.content import TextBlock
@dataclasses.dataclass
class Badge(Component):
text: str
foreground: str = "cyan"
def compose(self, ctx): # (1)!
return TextBlock.from_plain(self.text, foreground=self.foreground)
- The paint hook is how a component turns attributes into content. Built-ins do the same under the hood; you only write it for custom types.
Drop it in a field like any other component:
Or paint it alone: render(Badge("ok")).
Focus and grid hooks¶
Interactive pieces usually stay hook-driven on the grid, not buried only
inside the component. Give the field a group, then bind @on_click /
@on_keyboard on the grid — same as Events & Hooks.
from xnano import BaseGrid, Field, on_click, on_keyboard
from xnano.components.button import Button
class Form(BaseGrid):
submit: Button = Field(
default_factory=lambda: Button(label="Save"),
group="submit",
)
@on_click(group="submit")
@on_keyboard("enter", group="submit")
def save(self, ctx) -> None:
...
Button leaves activation keys free so those hooks fire. Inputs can declare
passthrough / submit_keys for the same reason: app shortcuts keep working
while the user types.
Override handle_keyboard / handle_paste on a custom component when the
widget itself should consume keys (editors, lists). Return True only when
you handled the event.
Built-in set¶
| Component | Module | Typical use |
|---|---|---|
Text |
xnano.components.text |
Labels, nested runs, optional input mode |
Input |
xnano.components.input |
Single-line / multiline editor |
Options / Select |
xnano.components.options |
Browseable list (Select is an alias) |
Button |
xnano.components.button |
Focusable control + grid hooks |
Table |
xnano.components.table |
Rows of data; subclass with Column |
Chart |
xnano.components.chart |
Plots; subclass with Series |
Bar |
xnano.components.bar |
Compact sample series |
Loader |
xnano.components.loader |
Spinner or determinate bar/line |
Dropdown |
xnano.components.dropdown |
Dropdown control |
Image |
xnano.components.image |
Image (images extra) |
Link |
xnano.components.link |
Link text |
Markdown |
xnano.components.markdown |
Markdown in a field |
Scrollbar |
xnano.components.scrollbar |
Scroll chrome |
Column / Series |
xnano.components.schema |
Descriptors for table/chart subclasses |
Component |
xnano.components.component |
Base for custom widgets |
For paging a whole file as a session, see Markdown — that is the document runner, not the field component.
Grids vs components¶
| Grid | Component | |
|---|---|---|
| Role | Layout + hooks + nested structure | Content (and optional focus) in a slot |
| Nesting | Other grids as fields | Nested content / descriptors inside one type |
| Events | @on_* on methods |
Optional handle_*; app hooks still on the grid |
| State | Field(state=True), app state= |
Live attributes on the instance |
Use a nested grid when you need regions, focus groups, and hooks as a unit. Use a component when the unit is “one widget with its own data shape.”
Next¶
- Fields — slots that hold components
- State — keep models and views in sync
- Events & Hooks — wire interaction on the grid
- Styling — colors and
class_nameon fields
API
Component ·
Text ·
Table ·
Chart ·
Column / Series ·
components index