# A View of params and actions
URL: https://lsd.tools/docs/ui-panels
Updated: 2026-07-18

> A View with no kind is param() fields plus @action buttons — the base View, for collecting input and running actions (add a kind and it becomes a data view). There is no separate Panel type; "panel" is just the old informal word for one docked in the sidebar, and sidebar is only a location. Author it with @tool.view or a plain View subclass. An @action handler reads the fields as typed attributes (self.cutoff), and self.ctx is auto-injected on mount so it can call self.ctx.ui.notify(...). invoke("name") is the one dispatch entry — button, CLI, agent, or test — and View.show() presents it standalone (a window, or console text when headless).

The base [`View`](/docs/ui-overview) has no `kind`: it is `param()` fields plus `@action` buttons —
the same declarative object the inspector uses to edit step settings. It collects input and runs
actions; give it a `kind` and it becomes a [data view](/docs/ui-widgets) instead. An `@action`
handler reads the fields as typed attributes, so there is no values dict to keep in sync. (Where it
shows — sidebar, editor, or an overlay — is `location=`, separate from what it is. A no-kind View
docked in the sidebar is what people used to call a "panel".)

## Author a View

Subclass `View`, declare `param()` fields, and mark operations with `@action`:

```python
from lsdtools import View, action, param

class ClipPanel(View):
    cutoff = param(float, default=0.5, min=0.0, max=1.0, label="Cut-off")

    @action(label="Apply", style="suggested")
    def apply(self):
        return f"keeping grades ≥ {self.cutoff}"

panel = ClipPanel(cutoff=0.7)
panel.invoke("apply")          # → 'keeping grades ≥ 0.7'
```

`invoke("apply")` is the **one dispatch entry** for every front-end — a GUI button, the CLI, an
agent, or a test all go through it. `param()` gives you typing, coercion, min/max validation, and
the labelled field for free; `@action`'s `style` is `None`, `"suggested"`, or `"destructive"`.

## Dock it with @tool.view

`@tool.view` decorates a function `(ctx) -> View` (return a `View` instance or class). With
`location="sidebar"` the desktop docks the result in the sidebar. When it mounts, the runtime injects
the host `Context` into the View's `self.ctx`, so an action body can reach the host —
`self.ctx.ui.notify(...)`, `self.ctx.ui.show(...)` — with no manual wiring:

```python
from lsdtools import Tool, Context, View, action, param

tool = Tool("grade", label="Grade tools")

class ClipPanel(View):
    cutoff = param(float, default=0.5, min=0.0, max=1.0, label="Cut-off")

    @action(label="Apply", style="suggested")
    def apply(self):
        self.ctx.ui.notify(f"cut-off set to {self.cutoff}")

@tool.view("grade.clip", title="Clip grades", location="sidebar")
def clip_panel(ctx: Context):
    return ClipPanel()
# → a "Clip grades" panel docks in the sidebar
```

`self.ctx.ui` offers `notify(message, level="info")`, `show(view)` (open a View as a modal), and
`confirm(message)`. On a standalone View you build yourself, `self.ctx` is `None` until you set it.

## Show it standalone

Any View presents itself with `show()` — a window when a front-end is loaded, a console rendering
when headless — so you can exercise the form without the desktop:

```python
ClipPanel(cutoff=0.7).show("text")
# ── ClipPanel ──
#   Cut-off: 0.7
#   actions: Apply
```

`show()` is the View's own verb; attach it to the engine instead with `engine.add(ClipPanel())` and
`engine.show()` to present it alongside your other [views](/docs/ui-overview).

## Overlays

`@tool.view(location="overlay")` is the same idea docked over the 3D viewer as a HUD: its function
also returns a `View` (typically an action-only one), and the desktop reads the `dock` / `view_kind`
hints to place it. Everything on this page applies — an overlay is just a View shown in a different
spot.

## Related

**Learn** — [Everything you show is a View](/docs/ui-overview) · [Data views](/docs/ui-widgets) · [Commands & actions](/docs/tools-commands-actions)

**Start** — [How an LSD package fits together](/docs/start-architecture) · [Naming: which is which](/docs/tools-glossary)

**Examples** — [A dockable panel](/docs/ex-panel) · [A complete package](/docs/ex-full-package)
</content>

## FAQ
### How does an action get at the host (notify, show a dialog)?

Through self.ctx. When a @tool.view is mounted, the runtime injects the host Context into self.ctx, so an @action body can call self.ctx.ui.notify(...) or self.ctx.ui.show(...) with no wiring. On a standalone View you construct yourself, self.ctx is None until you set it.

### Is there still a Panel or Field class?

No. The old JSON ui()/Panel/Field builder was removed, and there is no Panel class or @tool.panel decorator. A View with no kind is just a View — param() for fields, @action for buttons — the same object the desktop inspector already edits. "Panel" is only the old informal word for one docked in the sidebar; where it shows is location= (sidebar / editor / overlay), separate from what it is.
