Metadata-Version: 2.5
Name: anylumino
Version: 0.2.0
Summary: Lumino layout widgets for composing anywidgets.
Project-URL: Homepage, https://github.com/faysou/anylumino
Project-URL: Repository, https://github.com/faysou/anylumino
Project-URL: Issues, https://github.com/faysou/anylumino/issues
Author: Faysal Aberkane
License-Expression: MIT
License-File: LICENSE
License-File: THIRD_PARTY_LICENSES.md
Requires-Python: >=3.14
Requires-Dist: anywidget>=0.11.0
Requires-Dist: traitlets>=5.15.1
Description-Content-Type: text/markdown

# anylumino

`anylumino` is a Python library for building composable notebook widgets with
anywidget. It provides Lumino layout panels, menus, and toolbars, a family of
Astryx UI components for inputs, tables, and surfaces, and browser-native date
and time controls, all exposed as anywidgets that nest into one another.

It targets the anywidget 0.11 AFM composition API: Python passes child
anywidgets as synced widget references, and the frontend resolves each child
with `host.getWidget(...)` before mounting it into a Lumino layout widget or an
Astryx component slot.

The main benefit is composition. `anylumino` lets a notebook author build
larger reusable widgets from existing anywidgets, Astryx controls, charts,
tables, and outputs. A composed widget can own its layout, state, callbacks,
and domain methods while still being an anywidget that can be nested into
another layout.

It also ships a workbench that builds these UIs by chatting with a local coding
agent, Claude Code or Codex, and exports the result as plain Python.

Documentation: <https://faysou.github.io/anylumino/docs/>

## Quick start

```python
import anylumino as al
import anylumino.astryx as ax

brand = ax.Brand(
    "desk",
    **{
        "color-accent": ("#0057b8", "#79b8ff"),
        "color-background-card": ("#ffffff", "#111827"),
        "color-text-primary": ("#111827", "#f9fafb"),
        "radius-container": "8px",
    },
)

status = ax.Text("Ready")

orders = ax.Table(
    rows=[
        {"order_id": "O-1", "symbol": "AAPL", "qty": 10, "status": "NEW"},
        {"order_id": "O-2", "symbol": "MSFT", "qty": 5, "status": "PARTIAL"},
    ],
    columns={"order_id": "Order", "symbol": "Symbol", "qty": "Qty", "status": "Status"},
    row_key="order_id",
    selects="multiple",
    sortable=True,
    sort_key="symbol",
    width="100%",
)

controls = ax.Theme(
    {
        "heading": ax.Stack(
            {
                "title": ax.Heading("Orders", level=3),
                "badge": ax.Badge("Live", variant="success"),
            },
            direction="horizontal",
            gap=2,
            align="center",
        ),
        "symbol": ax.Typeahead(
            ["AAPL", "MSFT", "NVDA"],
            value="AAPL",
            label="Symbol",
        ),
        "fields": ax.Tokenizer(
            ["Bid", "Ask", "Last", "Size"],
            value=["Bid", "Ask"],
            label="Fields",
        ),
        "refresh": ax.Button(
            "Refresh",
            variant="primary",
            callbacks=[lambda _button: setattr(status, "text", "Refreshed")],
        ),
        "orders": orders,
    },
    brand=brand,
    mode="system",
    gap=3,
)

al.SplitPanel(
    {"controls": controls, "status": status},
    orientation="vertical",
    sizes=[0.8, 0.2],
    height=560,
)
```

## What it provides

- Layout widgets: public `LayoutWidget` base plus `TabPanel`, `BoxPanel`,
  `HBox`, `VBox`, `ScrollBox`, `SplitPanel`, `DockPanel`, `AccordionPanel`,
  `StackedPanel`, `GridPanel`, and `ResponsivePanel`.
- Astryx layout panels with the same keyed child API and Astryx styling:
  `TabPanel`, `StackedPanel`, `AccordionPanel`, `ScrollBox`, `SplitPanel`,
  and `ResponsivePanel` in `anylumino.astryx`.
- Astryx component wrappers for notebook UI: buttons, text, headings, badges,
  cards, stacks, grids, forms, fields, tabs, segmented controls, selectors,
  checkboxes, radio lists, sliders, date/time inputs, banners, status dots,
  progress bars, code, Markdown, tooltips, popovers, inline dialogs, static
  search inputs, command palettes, and tables.
- Astryx table helpers: normalized rows and columns, optional checkbox
  selection, sorting, row append/prepend/update/remove helpers, and Python
  callbacks for selection and sort changes.
- Reusable Astryx theming with `Brand` and `Theme`, so notebooks can
  define shared colors, radii, and component treatment once.
- Action widgets: `Toolbar`, `DropdownMenu`, `MoreMenu`, and
  `CommandPalette` for notebook-safe commands.

All layout containers inherit from `LayoutWidget`, which provides keyed child
composition, owner access, dynamic mutation, and method forwarding.

This turns layout into an application structure, not only visual placement.
Children remain addressable by key, callbacks can interact with sibling
widgets, and larger components such as `OrderDashboard`, `FigurePanel`, or
`SampleReview` can be reused across notebooks without relying on global
variables.

## Workbench

`anylumino.workbench.Workbench` builds a notebook UI from a chat with a local
`claude` (Claude Code) or `codex` (Codex CLI). Pass the data the UI should use,
then describe the UI:

```python
import pandas as pd
from anylumino.workbench import Workbench

sales = pd.DataFrame(
    {"region": ["EMEA", "APAC", "AMER"], "revenue": [1.5, 2.0, 3.2]}
)

Workbench(agent="claude", session="sales", namespace={"sales": sales})
```

The agent writes an OpenUI program and an `App` class with the callbacks. The
workbench shows the live UI next to the program, the Python generated from it,
and the callbacks. The agent sees the names, types, and data frame columns of
the namespace, not its rows, and its turns run read-only. Charts use Plotly.

**Export** writes the UI to one Python file, which
`anylumino.workbench.load("exports/sales.py", sales=sales)` shows in any
notebook without the agent. **Import** reads an edited file back into the
workbench. The workbench needs `node` on `PATH` and a signed-in agent CLI. See
the [workbench guide](https://faysou.github.io/anylumino/docs/workbench/).

## Layout composition

Layout widgets are resizable by drag and drop by default. Pass
`resizable=False` to hide the bottom-right resize handle. `SplitPanel` also
syncs Lumino split-handle changes back to its `sizes` trait.

Layout children can be declared with semantic keys. If `titles` is omitted for
a mapping, the titles default to those keys.

```python
tabs = al.TabPanel({"orders": orders, "logs": ax.Text("No events")})
tabs.select_key("orders")
tabs.selected_key
tabs.get_widget("logs")
```

If a child is a Python wrapper with a `.widget` anywidget view, anylumino renders
the view and keeps the wrapper as the keyed owner.

```python
tabs = al.TabPanel({"trend": trend_panel})
tabs.get_widget("trend")  # trend_panel.widget
tabs["trend"].refresh()
tabs.call_owner("trend", "refresh")
```

To add methods to a composed UI object, subclass a layout widget. The subclass
is still an anywidget and can be nested inside another layout:

```python
class OrderDashboard(al.VBox):
    def __init__(self, rows):
        self.table = ax.Table(
            rows,
            row_key="order_id",
            selects="multiple",
            sortable=True,
        )
        super().__init__({"table": self.table})

    def replace_orders(self, rows):
        self.table.set_rows(rows)


tabs = al.TabPanel({"orders": OrderDashboard(order_rows)})
tabs["orders"].replace_orders(next_order_rows)
```

## Astryx widgets

Astryx widgets are the UI family for notebook-facing controls. They use the
`anylumino.astryx` namespace so notebook component code stays explicit. Useful
appearance and behavior options are keyword-only constructor arguments with
Python snake-case names. Unmodeled JSON-safe upstream options can still be
passed through `**props`. See the [Astryx component documentation](https://astryx.atmeta.com/components)
for the upstream component reference.

```python
form = ax.FormLayout(
    {
        "symbol": ax.TextInput(value="AAPL", label="Symbol"),
        "quantity": ax.NumberInput(value=100, label="Quantity", min=1),
        "side": ax.SegmentedControl(
            ["Buy", "Sell"],
            value="Buy",
            label="Side",
        ),
        "routing": ax.Selector(
            ["Smart", "Primary", "Dark"],
            value="Smart",
            label="Routing",
        ),
        "urgent": ax.Checkbox(value=False, label="Urgent"),
    }
)
```

Read an input widget's current value with `.value`. Use traitlets observers
when another widget should react to edits:

```python
symbol = ax.TextInput(value="AAPL", label="Symbol")
quantity = ax.NumberInput(value=100, label="Quantity")

symbol.value
quantity.value

symbol.observe(lambda change: print(change["new"]), names="value")
```

Text inputs, number inputs, and sliders send every keystroke and drag step to
Python. Pass `continuous_update=False` when a value should arrive only once the
edit ends, which for inputs means blur or Enter and for sliders means the end of
the drag.

```python
notes = ax.TextArea(value="", label="Notes", continuous_update=False)
risk = ax.Slider(value=42, label="Risk budget", continuous_update=False)
```

Astryx `Slider` steps linearly, so two wrappers adapt it for scales it does not
model directly. `LogSlider` drives it in exponent space and reports
`base ** exponent`; `SelectionSlider` drives it over option indices and reports
the option, taking a two-item value for a range.

```python
rate = ax.LogSlider(1e-3, label="Learning rate", base=10, min_exponent=-4, max_exponent=0)
size = ax.SelectionSlider(["XS", "S", "M", "L"], value="M", label="Size", marks=True)
window = ax.SelectionSlider(["Mon", "Tue", "Wed", "Thu", "Fri"], value=["Tue", "Thu"], label="Window")
```

To mirror one widget's state onto another, prefer `traitlets.link` over a
callback:

```python
import traitlets

mirror = ax.Text("AAPL")
traitlets.link((symbol, "value"), (mirror, "text"))
```

Every anylumino widget registers activations the same way: `on_click(callback)`
runs for each activation and receives the widget, while `on_action(callback)`
runs only for activations that carry a value and receives the widget and that
value. Pass `remove=True` to unregister the same callable. The `callbacks` and
`action_callbacks` constructor arguments register the same two lists.

What counts as an activation depends on the widget kind:

- Event widgets such as buttons, cards, menus, toolbars, and command palettes
  activate on a click, because a click is not a persistent value.
- Value widgets such as checkboxes, switches, inputs, sliders, and the native
  date and time controls activate on every `value` change. `on_action` receives
  the new value. They also fire when Python assigns `value`, so use `observe`
  when a handler needs the old value or must ignore programmatic updates.
- Tables and tab panels keep selection, sort, and tab state in synced traits.
  Observe those traits, or use `on_selection` and `on_sort` on tables.

```python
status = ax.Text("Ready")
button = ax.Button(
    "Submit",
    variant="primary",
    callbacks=[lambda _button: setattr(status, "text", "Submitted")],
)

live = ax.Checkbox(False, label="Live")
live.on_action(lambda _checkbox, checked: setattr(status, "text", f"Live: {checked}"))

menu = ax.DropdownMenu(
    [{"label": "Export", "value": "export"}],
    label="Actions",
)
menu.on_action(lambda _menu, action: setattr(status, "text", f"Chose {action}"))
```

Choosing a menu item is both an activation and a value, so it runs `on_click`
and `on_action`. `Toolbar`, `MenuBar`, and `CommandPalette` also keep their
id-keyed `callbacks` mapping, which stays mutable after construction.

## Astryx tables

`Table` renders structured row data with optional checkbox selection and
sorting. Pass mappings, sequences, or scalar rows; use `row_key` when mappings
contain a stable row id such as an order id or symbol.

```python
orders = ax.Table(
    rows=[
        {"order_id": "O-1", "symbol": "AAPL", "qty": 10, "status": "NEW"},
        {"order_id": "O-2", "symbol": "MSFT", "qty": 5, "status": "PARTIAL"},
    ],
    columns={"order_id": "Order", "symbol": "Symbol", "qty": "Qty", "status": "Status"},
    row_key="order_id",
    selects="multiple",
    sortable=True,
    sort_key="symbol",
)

orders.prepend_row({"order_id": "O-0", "symbol": "AMD", "qty": 2, "status": "NEW"})
orders.append_row({"order_id": "O-3", "symbol": "NVDA", "qty": 1, "status": "NEW"})
orders.update_row("O-2", {"qty": 7, "status": "FILLED"})
orders.remove_row("O-1")
orders.set_rows(next_order_snapshot)
```

Each row helper assigns a new `rows` list, so displayed notebooks receive the
update through traitlets. `prepend_row` inserts before existing rows and
`append_row` inserts after existing rows. Leave `selects=""` for no checkbox
column, use `selects="single"` for one selected row, or use
`selects="multiple"` for multi-row selection.

## Theming

Use `Brand` and `Theme` for small reusable notebook theme
overrides:

```python
brand = ax.Brand(
    "desk",
    **{
        "color-accent": ("#0057b8", "#79b8ff"),
        "color-background-card": ("#ffffff", "#111827"),
        "radius-container": "8px",
    },
)

panel = ax.Theme(
    {"button": ax.Button("Run"), "status": ax.Badge("Ready")},
    brand=brand,
    mode="system",
    gap=2,
)
```

`mode` accepts `"light"`, `"dark"`, `"system"` to follow the OS
`prefers-color-scheme`, and `"jupyterlab"` to follow the JupyterLab theme.
Astryx syncs the active mode to the page's `html[data-theme]`; anylumino restores
what the page started with once the last Astryx widget unmounts, so clearing one
output does not leave the notebook with no mode.

`Theme` propagates the brand to nested Astryx children because every
anywidget child is mounted in its own frontend root. See
`docs/astryx_brand_theme.md` for runtime `defineTheme()` themes,
precompiled `BuiltTheme` CSS themes, and the workflow for defining new
Astryx themes. That document also describes the frontend style injection,
deduplication, and JupyterLab testing workflow for theme changes.

## Assets

Frontend assets are bundled into the Python package. Runtime notebooks do not
need CDN access for Lumino, Astryx, or the bundled workflow icons.

Saving a notebook with widget state stores those assets once per widget model,
which makes such files large. Rerun the examples with a kernel instead, or
execute them with `--ExecutePreprocessor.store_widget_state=False`.

## Development

```sh
uv sync --group dev
npm ci
npm run build
uv run pytest
uv run jupyter lab
```

Build and browse the documentation site:

```sh
make docs-preview
```

This generates the Python API reference from source and NumPy docstrings, then opens
`http://127.0.0.1:4200/docs` with the Fumadocs development server. Use `make docs` for
a static export into `out/`, `make docs-serve` to serve that export locally, and
`make docs-check` to run API generation, link tests, linting, type checks, and the
production build. The Griffe-backed `fumadocs-python` generator owns
`content/docs/api`; update public signatures and docstrings in `src/anylumino`
instead of editing generated API pages.

The Docs workflow publishes the same export to GitHub Pages at
<https://faysou.github.io/anylumino/docs/> on every push to `main`.

Open `notebooks/astryx_components_smoke.py` for the broad Astryx component
smoke test. Open `notebooks/astryx_layout_panels.py` for the Astryx layout
panels driven from Python. Open `notebooks/astryx_table.py` for focused table
selection, sorting, and row-helper examples. Open `notebooks/plotly_tabs.py` and
`notebooks/plotly_dashboard.py` for Lumino layout patterns with Plotly figures.
Open `notebooks/itables_tabs.py` to see an `itables.widget.ITable` inside a tab
with controls that append rows to a pandas DataFrame and refresh the displayed
table.

## License

`anylumino` is MIT licensed and requires Python 3.14 or later. The frontend
bundles compile Lumino, Astryx, StyleX, and React into the wheel;
`THIRD_PARTY_LICENSES.md` lists those packages with their notices, and
`npm run licenses` regenerates it from the bundle inputs.
