Back to skills
SkillHub ClubShip Full StackFull Stack

castella-core

Build desktop, web, or terminal UIs with Castella. Create widgets, components, layouts, manage reactive state, handle events, and use the theme system.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
39
Hot score
90
Updated
March 20, 2026
Overall rating
C2.1
Composite score
2.1
Best-practice grade
C65.6

Install command

npx @skill-hub/cli install i2y-castella-castella-core

Repository

i2y/castella

Skill path: skills/castella-core

Build desktop, web, or terminal UIs with Castella. Create widgets, components, layouts, manage reactive state, handle events, and use the theme system.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: i2y.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install castella-core into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/i2y/castella before adding castella-core to shared team environments
  • Use castella-core for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: castella-core
description: Build desktop, web, or terminal UIs with Castella. Create widgets, components, layouts, manage reactive state, handle events, and use the theme system.
---

# Castella Core UI Development

Castella is a pure Python cross-platform UI framework for desktop (GLFW/SDL2), web (PyScript/Pyodide), and terminal (prompt-toolkit) applications. Write once, run everywhere with GPU-accelerated rendering via Skia.

**When to use**: "create a Castella app", "build a Castella UI", "Castella component", "add a button/input/text", "use reactive state", "layout with Row/Column", "change the theme", "handle click events", "preserve scroll position", "animate a widget"

## Quick Start

Create a minimal Castella app:

```python
from castella import App, Text
from castella.frame import Frame

App(Frame("Hello", 800, 600), Text("Hello, Castella!")).run()
```

Install and run:

```bash
uv sync --extra glfw   # Desktop with GLFW
uv run python app.py
```

## Core Concepts

### App and Frame

- `Frame(title, width, height)` - Window/container for the UI
- `App(frame, widget)` - Application entry point with `.run()`
- Frame auto-selects platform: GLFW (desktop), Web, or Terminal

```python
from castella import App
from castella.frame import Frame

frame = Frame("My App", 800, 600)
app = App(frame, my_widget)
app.run()
```

### Widgets

Base building blocks for UI elements:

| Widget | Description | Key Methods |
|--------|-------------|-------------|
| `Text(content)` | Display text | `.font_size(n)` |
| `Button(label)` | Clickable button | `.on_click(handler)` |
| `Input(initial)` | Single-line input | `.on_change(handler)` |
| `MultilineInput(state)` | Multi-line editor | `.on_change(handler)` |
| `CheckBox(state)` | Toggle checkbox | `.on_change(handler)` |
| `Slider(state)` | Range slider | `.on_change(handler)` |
| `Image(path)` | Local image | - |
| `NetImage(url)` | Remote image | - |
| `Markdown(content)` | Rich markdown | `.on_link_click(handler)` |

### Layout Containers

Arrange widgets hierarchically:

```python
from castella import Column, Row, Box

# Vertical stack
Column(
    Text("Header"),
    Button("Click me"),
    Text("Footer"),
)

# Horizontal stack
Row(
    Button("Left"),
    Button("Right"),
)

# Overlapping (z-index support)
Box(
    main_content,
    modal_overlay.z_index(10),
)
```

## Component Pattern

Build reactive UIs with the `Component` class:

```python
from castella import Component, State, Column, Text, Button

class Counter(Component):
    def __init__(self):
        super().__init__()
        self._count = State(0)
        self._count.attach(self)  # Trigger view() on change

    def view(self):
        return Column(
            Text(f"Count: {self._count()}"),
            Button("+1").on_click(lambda _: self._count.set(self._count() + 1)),
        )
```

### State Management

`State[T]` is an observable value that triggers UI rebuilds:

```python
from castella import State

count = State(0)           # Create with initial value
value = count()            # Read current value
count.set(42)              # Set new value
count += 1                 # Operator support: +=, -=, *=, /=
```

### ListState for Collections

`ListState` is an observable list:

```python
from castella import ListState

items = ListState(["a", "b", "c"])
items.append("d")          # Triggers rebuild
items.set(["x", "y"])      # Atomic replace (single rebuild)
```

### Multiple States Pattern

When using multiple states, attach each to the component:

```python
class MultiStateComponent(Component):
    def __init__(self):
        super().__init__()
        self._tab = State("home")
        self._counter = State(0)
        # Attach each state
        self._tab.attach(self)
        self._counter.attach(self)

    def view(self):
        return Column(
            Text(f"Tab: {self._tab()}"),
            Text(f"Count: {self._counter()}"),
        )
```

## Size Policies

Control how widgets size themselves:

| Policy | Behavior |
|--------|----------|
| `SizePolicy.FIXED` | Exact size specified |
| `SizePolicy.EXPANDING` | Fill available space |
| `SizePolicy.CONTENT` | Size to fit content |

### Fluent API Shortcuts

```python
from castella import SizePolicy

# Fixed sizing
widget.fixed_width(100)
widget.fixed_height(40)
widget.fixed_size(200, 100)

# Content sizing
widget.fit_content()          # Both dimensions
widget.fit_content_width()    # Width only
widget.fit_content_height()   # Height only

# Fill parent
widget.fit_parent()
```

### Important Constraint

A Layout with `CONTENT` height_policy cannot have `EXPANDING` height children:

```python
# This will raise RuntimeError:
Column(
    Text("Hello"),  # Text defaults to EXPANDING height
).height_policy(SizePolicy.CONTENT)

# Fix by setting children to FIXED or CONTENT:
Column(
    Text("Hello").fixed_height(24),
).height_policy(SizePolicy.CONTENT)
```

## Styling

### Widget Styling Methods

Chain style methods on widgets:

```python
Text("Hello")
    .bg_color("#1a1b26")
    .text_color("#c0caf5")
    .fixed_height(40)
    .padding(10)
```

### Border Styling

```python
# Show border with theme's default color (or custom color)
widget.show_border()              # Use theme's border color
widget.show_border("#ff0000")     # Use custom color

# Hide border (make it match background)
widget.erase_border()
```

### Theme System

Access and toggle themes:

```python
from castella.theme import ThemeManager

manager = ThemeManager()
theme = manager.current           # Get current theme
manager.toggle_dark_mode()        # Toggle dark/light
manager.prefer_dark(True)         # Force dark mode
```

Built-in themes: Tokyo Night (default), Cupertino, Material Design 3

See `references/theme.md` for custom themes.

## Event Handling

### Click Events

```python
Button("Click me").on_click(lambda event: print("Clicked!"))
```

### Input Changes

```python
Input("initial").on_change(lambda text: print(f"New value: {text}"))
```

### Important: Input Widget Pattern

Do NOT attach states that Input/MultilineInput manages:

```python
class FormComponent(Component):
    def __init__(self):
        super().__init__()
        self._text = State("initial")
        # DON'T attach - causes focus loss on every keystroke
        # self._text.attach(self)

    def view(self):
        return Input(self._text()).on_change(lambda t: self._text.set(t))
```

## Animation

### AnimatedState

Values that animate smoothly on change:

```python
from castella import AnimatedState

class AnimatedCounter(Component):
    def __init__(self):
        super().__init__()
        self._value = AnimatedState(0, duration_ms=300)
        self._value.attach(self)

    def view(self):
        return Column(
            Text(f"Value: {self._value():.1f}"),
            Button("+10").on_click(lambda _: self._value.set(self._value() + 10)),
        )
```

### Widget Animation Methods

```python
# Animate to position/size
widget.animate_to(x=200, y=100, duration_ms=400)

# Slide animations
widget.slide_in("left", distance=100, duration_ms=300)
widget.slide_out("right", distance=100, duration_ms=300)
```

See `references/animation.md` for more animation patterns.

## Scrollable Containers

Make layouts scrollable:

```python
from castella import Column, ScrollState, SizePolicy

class ScrollableList(Component):
    def __init__(self, items):
        super().__init__()
        self._items = ListState(items)
        self._items.attach(self)
        self._scroll = ScrollState()  # Preserves scroll position

    def view(self):
        return Column(
            *[Text(item).fixed_height(30) for item in self._items],
            scrollable=True,
            scroll_state=self._scroll,
        ).fixed_height(300)
```

## Z-Index Stacking

Layer widgets with z-index:

```python
from castella import Box

Box(
    main_content.z_index(1),
    modal_dialog.z_index(10),  # Appears on top
)
```

## Semantic IDs for MCP

Assign semantic IDs for MCP accessibility:

```python
Button("Submit").semantic_id("submit-btn")
Input("").semantic_id("email-input")
```

## Best Practices

1. **Attach states**: Use `state.attach(self)` for each observable state
2. **Fixed heights in scrollable containers**: Use `.fixed_height()` for list items
3. **Preserve scroll**: Use `ScrollState` to maintain scroll position
4. **Atomic list updates**: Use `ListState.set(items)` for single rebuild
5. **Don't attach Input states**: Avoid attaching states managed by Input widgets
6. **Semantic IDs**: Add `.semantic_id()` for MCP integration

## Running Scripts

```bash
# Counter example
uv run python scripts/counter.py

# Hot reload during development
uv run python tools/hot_restarter.py scripts/counter.py
```

## Packaging

Package your Castella app for distribution:

```bash
# Install ux bundler
uv tool install ux-py

# Create executable
ux bundle --project . --output ./dist/
```

See `castella-packaging` skill for detailed options (macOS app bundles, code signing, cross-compilation).

## Reference

- `references/widgets.md` - Complete widget API
- `references/theme.md` - Theme system details
- `references/animation.md` - Animation patterns
- `references/state.md` - State management patterns
- `scripts/` - Executable examples (counter.py, form.py, scrollable_list.py)


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/counter.py

```python
"""
Basic Counter Example - Demonstrates Component pattern with State.

Run with: uv run python skills/castella-core/examples/counter.py
"""

from castella import App, Component, State, Column, Row, Text, Button
from castella.frame import Frame


class Counter(Component):
    """A simple counter component demonstrating State and event handling."""

    def __init__(self):
        super().__init__()
        self._count = State(0)
        self._count.attach(self)  # Trigger view() on state change

    def view(self):
        return Column(
            Text(f"Count: {self._count()}").font_size(24),
            Row(
                Button("-1").on_click(self._decrement).fixed_width(80),
                Button("+1").on_click(self._increment).fixed_width(80),
            ),
            Button("Reset").on_click(self._reset),
        ).padding(20)

    def _increment(self, _event):
        self._count += 1

    def _decrement(self, _event):
        self._count -= 1

    def _reset(self, _event):
        self._count.set(0)


if __name__ == "__main__":
    App(Frame("Counter", 400, 200), Counter()).run()

```

### references/theme.md

```markdown
# Castella Theme System

Comprehensive theming with design tokens for consistent styling.

## ThemeManager

Singleton for theme management:

```python
from castella.theme import ThemeManager

manager = ThemeManager()

# Get current theme
theme = manager.current
print(f"{theme.name}, dark={theme.is_dark}")

# Toggle dark/light mode
manager.toggle_dark_mode()

# Force dark mode
manager.prefer_dark(True)
```

## Built-in Themes

### Tokyo Night (Default)
Purple/blue aesthetic with 6px rounded corners.

```python
from castella.theme import TOKYO_NIGHT_DARK_THEME, TOKYO_NIGHT_LIGHT_THEME

manager.set_dark_theme(TOKYO_NIGHT_DARK_THEME)
manager.set_light_theme(TOKYO_NIGHT_LIGHT_THEME)
```

### Cupertino
Apple-inspired design with 8px rounded corners.

```python
from castella.theme import CUPERTINO_DARK_THEME, CUPERTINO_LIGHT_THEME

manager.set_dark_theme(CUPERTINO_DARK_THEME)
manager.set_light_theme(CUPERTINO_LIGHT_THEME)
```

### Material Design 3
Google's Material design with 12px rounded corners.

```python
from castella.theme import MATERIAL_DARK_THEME, MATERIAL_LIGHT_THEME

manager.set_dark_theme(MATERIAL_DARK_THEME)
manager.set_light_theme(MATERIAL_LIGHT_THEME)
```

### Classic Castella
Original neon/pastel themes.

```python
from castella.theme import DARK_THEME, LIGHT_THEME
```

## Theme Properties

### Colors (ColorPalette)

```python
theme = manager.current

# Background colors
theme.colors.bg_canvas      # Main background
theme.colors.bg_primary     # Primary surface
theme.colors.bg_secondary   # Secondary surface
theme.colors.bg_tertiary    # Tertiary surface

# Text colors
theme.colors.text_primary   # Main text
theme.colors.text_secondary # Secondary text
theme.colors.text_muted     # Muted text

# Semantic colors
theme.colors.text_info      # Info blue
theme.colors.text_success   # Success green
theme.colors.text_warning   # Warning yellow
theme.colors.text_danger    # Danger red

# Border colors
theme.colors.border_primary
theme.colors.border_secondary
```

### Typography

```python
theme.typography.font_family       # Default font
theme.typography.font_family_mono  # Monospace font
theme.typography.base_size         # Base font size (px)
theme.typography.scale_ratio       # Size scaling ratio
```

### Spacing

```python
theme.spacing.padding_sm    # Small padding
theme.spacing.padding_md    # Medium padding
theme.spacing.padding_lg    # Large padding
theme.spacing.margin_sm     # Small margin
theme.spacing.margin_md     # Medium margin
theme.spacing.margin_lg     # Large margin
theme.spacing.border_radius # Corner radius
theme.spacing.border_width  # Border width
```

## Creating Custom Themes

### Derive from Existing Theme

Partial override of an existing theme:

```python
from castella.theme import TOKYO_NIGHT_DARK_THEME

custom = TOKYO_NIGHT_DARK_THEME.derive(
    colors={
        "border_primary": "#00ff00",
        "text_info": "#00ffff",
    },
    typography={"base_size": 16},
    spacing={"border_radius": 12},
)

manager.set_dark_theme(custom)
```

### Create New Theme

Complete custom theme:

```python
from castella.theme import Theme, ColorPalette, Typography, Spacing

my_palette = ColorPalette(
    bg_canvas="#0a0a0a",
    bg_primary="#121212",
    bg_secondary="#1a1a1a",
    bg_tertiary="#242424",
    text_primary="#ffffff",
    text_secondary="#b0b0b0",
    text_muted="#707070",
    text_info="#64b5f6",
    text_success="#81c784",
    text_warning="#ffb74d",
    text_danger="#e57373",
    border_primary="#333333",
    border_secondary="#444444",
)

my_theme = Theme(
    name="my-custom-theme",
    is_dark=True,
    colors=my_palette,
    typography=Typography(
        font_family="Inter",
        font_family_mono="JetBrains Mono",
        base_size=14,
        scale_ratio=1.25,
    ),
    spacing=Spacing(
        padding_sm=4,
        padding_md=8,
        padding_lg=16,
        margin_sm=4,
        margin_md=8,
        margin_lg=16,
        border_radius=8,
        border_width=1,
    ),
    code_pygments_style="monokai",
)

manager.set_dark_theme(my_theme)
```

## Widget Styles

Themes provide default styles for widgets:

```python
theme.button        # Button styles by Kind and AppearanceState
theme.input         # Input field styles
theme.text          # Text styles
theme.scrollbar     # Scrollbar styles
theme.scrollbox     # Scrollbox container styles
```

## Environment Variables

Override theme at runtime:

```bash
CASTELLA_DARK_MODE=true   # Force dark mode
CASTELLA_DARK_MODE=false  # Force light mode
```

## Theme Demos

Run built-in theme demos:

```bash
uv run python examples/tokyo_night_theme_demo.py
uv run python examples/cupertino_theme_demo.py
uv run python examples/material_theme_demo.py
```

```

### references/animation.md

```markdown
# Castella Animation System

Smooth property animations with easing functions and reactive state transitions.

## AnimationScheduler

Singleton managing the animation tick loop (60 FPS desktop, 10 FPS TUI):

```python
from castella.animation import AnimationScheduler

scheduler = AnimationScheduler.get()
scheduler.add(tween)     # Add animation
```

## Easing Functions

Available easing functions:

```python
from castella.animation import EasingFunction

EasingFunction.LINEAR           # Constant speed
EasingFunction.EASE_IN          # Slow start
EasingFunction.EASE_OUT         # Slow end
EasingFunction.EASE_IN_OUT      # Slow start and end
EasingFunction.EASE_IN_CUBIC    # Cubic slow start
EasingFunction.EASE_OUT_CUBIC   # Cubic slow end
EasingFunction.EASE_IN_OUT_CUBIC  # Cubic slow start and end
EasingFunction.BOUNCE           # Bouncy effect
```

## Tween

Animate widget properties:

```python
from castella.animation import Tween, AnimationScheduler, EasingFunction

tween = Tween(
    target=my_widget,
    property_name="x",      # "x", "y", "width", "height"
    from_value=0,
    to_value=200,
    duration_ms=500,
    easing=EasingFunction.EASE_OUT_CUBIC,
    on_complete=lambda: print("Done!"),
)
AnimationScheduler.get().add(tween)
```

## ValueTween

Generic value interpolation with callbacks:

```python
from castella.animation import ValueTween, AnimationScheduler

def on_update(value):
    my_state.set(value)

tween = ValueTween(
    from_value=0,
    to_value=100,
    duration_ms=500,
    on_update=on_update,
    on_complete=lambda: print("Animation complete!"),
)
AnimationScheduler.get().add(tween)
```

## AnimatedState

State wrapper that automatically animates between values:

```python
from castella import Component, Column, Text, Button
from castella.animation import AnimatedState

class Counter(Component):
    def __init__(self):
        super().__init__()
        # Value changes animate smoothly over 200ms
        self._value = AnimatedState(0, duration_ms=200)
        self._value.attach(self)

    def view(self):
        return Column(
            Text(f"Value: {self._value():.1f}"),
            Button("Add 10").on_click(lambda _: self._value.set(self._value() + 10)),
        )
```

### AnimatedState Control

```python
state = AnimatedState(0, duration_ms=300)

state.set(100)                    # Animate to value (default)
state.set(100, animate=True)      # Explicit animate
state.set_immediate(100)          # Set without animation
state.stop()                      # Stop at current value
state.finish()                    # Jump to target value
```

## Widget Animation Methods

Convenient animation methods on widgets:

### animate_to

Animate to target position/size:

```python
widget.animate_to(x=200, y=100, duration_ms=400)
widget.animate_to(width=300, height=200, easing=EasingFunction.BOUNCE)
```

### slide_in / slide_out

Slide animations from/to directions:

```python
# Slide in from direction
widget.slide_in("left", distance=100, duration_ms=300)
widget.slide_in("right", distance=100, duration_ms=300)
widget.slide_in("top", distance=100, duration_ms=300)
widget.slide_in("bottom", distance=100, duration_ms=300)

# Slide out to direction
widget.slide_out("left", distance=100, duration_ms=300)
widget.slide_out("right", distance=100, duration_ms=300)
```

## Animation Control

### Cancel Animation

```python
tween = Tween(...)
AnimationScheduler.get().add(tween)

# Later, cancel if needed
tween.cancel()
```

## Animation Patterns

### Fade In Effect

```python
from castella.animation import ValueTween, AnimationScheduler

def fade_in(widget, duration_ms=300):
    def update_opacity(value):
        widget.opacity = value

    AnimationScheduler.get().add(
        ValueTween(0, 1, duration_ms, on_update=update_opacity)
    )
```

### Staggered Animations

```python
import time
from castella.animation import Tween, AnimationScheduler

def stagger_slide_in(widgets, delay_ms=50):
    for i, widget in enumerate():
        widget.x = -100  # Start off-screen

        def delayed_animation(w=widget, d=i * delay_ms):
            time.sleep(d / 1000)
            AnimationScheduler.get().add(
                Tween(w, "x", -100, 0, 300, EasingFunction.EASE_OUT)
            )

        threading.Thread(target=delayed_animation).start()
```

### Progress Animation

```python
from castella.animation import ValueTween, AnimationScheduler

def animate_progress(state, target_value, duration_ms=500):
    current = state.value()

    AnimationScheduler.get().add(
        ValueTween(
            current, target_value, duration_ms,
            on_update=lambda v: state.set(v),
        )
    )
```

```

### references/widgets.md

```markdown
# Castella Widget Reference

Complete API reference for all Castella widgets.

## Text Widgets

### Text
Display single-line text.

```python
from castella import Text

text = Text("Hello, World!")
text = Text("Styled").font_size(18).text_color("#ffffff")
```

**Methods:**
- `.font_size(size: int)` - Set font size in pixels
- `.text_color(color: str)` - Set text color (hex or named)
- `.bg_color(color: str)` - Set background color

### SimpleText
Lightweight text for performance-critical lists.

```python
from castella import SimpleText

text = SimpleText("Fast text", font_size=14)
```

### MultilineText
Read-only multi-line text with selection support.

```python
from castella.multiline_text import MultilineText

text = MultilineText("Line 1\nLine 2\nLine 3", font_size=14, wrap=True)
```

**Features:**
- Mouse drag to select text
- Cmd+C/Ctrl+C to copy
- Cmd+A/Ctrl+A to select all

## Input Widgets

### Input
Single-line text input with cursor positioning.

```python
from castella import Input
from castella.core import State

text = State("")
input_widget = Input(text()).on_change(lambda t: text.set(t))
```

**Methods:**
- `.on_change(handler: Callable[[str], None])` - Text change callback
- `.placeholder(text: str)` - Placeholder text

### MultilineInput
Multi-line text editor with scrolling.

```python
from castella.multiline_input import MultilineInput, MultilineInputState

state = MultilineInputState("Initial text")
editor = MultilineInput(state, font_size=14, wrap=True)
editor = editor.height(200).height_policy(SizePolicy.FIXED)
```

**Features:**
- Scrollbar for overflow content
- Mouse wheel scrolling
- Click to position cursor
- Text selection with mouse drag
- Copy/Cut/Paste (Cmd/Ctrl+C/X/V)
- Select all (Cmd/Ctrl+A)

## Button Widgets

### Button
Clickable button with semantic kinds.

```python
from castella import Button, Kind

button = Button("Click me").on_click(lambda e: print("Clicked"))
button = Button("Danger", kind=Kind.DANGER)
```

**Kinds:**
- `Kind.NORMAL` - Default styling
- `Kind.INFO` - Blue/cyan
- `Kind.SUCCESS` - Green
- `Kind.WARNING` - Yellow/orange
- `Kind.DANGER` - Red

### CheckBox
Toggle checkbox with optional labels.

```python
from castella import CheckBox
from castella.core import State

checked = State(False)
checkbox = CheckBox(checked).on_change(lambda v: print(f"Checked: {v}"))
checkbox = CheckBox(checked, on_label="ON", off_label="OFF")
checkbox = CheckBox(checked, is_circle=True)  # Circle style
```

### Switch
Toggle switch (same API as CheckBox).

```python
from castella import Switch
from castella.core import State

enabled = State(True)
switch = Switch(enabled).on_change(lambda v: print(f"Enabled: {v}"))
```

### RadioButtons
Single-select from options.

```python
from castella import RadioButtons
from castella.core import State

selected = State("option1")
radios = RadioButtons(
    ["option1", "option2", "option3"],
    selected,
).on_change(lambda v: print(f"Selected: {v}"))
```

## Slider Widget

### Slider
Range slider with state.

```python
from castella import Slider, SliderState

state = SliderState(value=50, min_val=0, max_val=100)
slider = Slider(state).on_change(lambda v: print(f"Value: {v}"))

# Access value
print(state.value())   # 50
print(state.ratio())   # 0.5 (normalized 0-1)
state.set(75)
```

## Progress Widget

### ProgressBar
Progress indicator.

```python
from castella import ProgressBar, ProgressBarState

state = ProgressBarState(value=0, min_val=0, max_val=100)
progress = ProgressBar(state)
progress = progress.track_color("#1a1b26").fill_color("#9ece6a")

state.set(75)  # Update progress
```

## Date/Time Widget

### DateTimeInput
Date and time picker with calendar.

```python
from castella import DateTimeInput, DateTimeInputState
from datetime import date

state = DateTimeInputState(
    value="2024-12-25T14:30:00",
    enable_date=True,
    enable_time=True,
)
picker = DateTimeInput(
    state=state,
    label="Appointment",
    min_date=date.today(),
)

# Get values
print(state.to_display_string())  # "2024-12-25 14:30"
print(state.to_iso())             # "2024-12-25T14:30:00"
```

## Image Widgets

### Image
Display local image.

```python
from castella import Image

img = Image("path/to/image.png")
```

### NetImage
Display remote image.

```python
from castella import NetImage

img = NetImage("https://example.com/image.png")
```

### AsyncNetImage
Async-loading remote image.

```python
from castella import AsyncNetImage

img = AsyncNetImage("https://example.com/image.png")
```

### NumpyImage
Display numpy array as image.

```python
from castella import NumpyImage
import numpy as np

array = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8)
img = NumpyImage(array)
```

## Layout Containers

### Column
Vertical layout.

```python
from castella import Column

col = Column(
    widget1,
    widget2,
    scrollable=True,      # Enable scrolling
    scroll_state=state,   # Preserve scroll position
)
```

### Row
Horizontal layout.

```python
from castella import Row

row = Row(widget1, widget2, widget3)
```

### Box
Overlapping layout with z-index.

```python
from castella import Box

box = Box(
    background.z_index(1),
    foreground.z_index(10),
)
```

## Navigation Widgets

### Tabs
Tabbed navigation.

```python
from castella import Tabs, TabsState, TabItem

state = TabsState([
    TabItem(id="home", label="Home", content=home_widget),
    TabItem(id="settings", label="Settings", content=settings_widget),
], selected_id="home")

tabs = Tabs(state).on_change(lambda id: print(f"Tab: {id}"))
state.select("settings")  # Programmatic selection
```

### Tree
Hierarchical tree view.

```python
from castella import Tree, TreeState, TreeNode

nodes = [
    TreeNode(id="docs", label="Documents", icon="📁", children=[
        TreeNode(id="readme", label="README.md", icon="📄"),
    ]),
]
state = TreeState(nodes, multi_select=False)
tree = Tree(state).on_select(lambda node: print(node.label))
```

### FileTree
File system tree.

```python
from castella import FileTree, FileTreeState

state = FileTreeState(root_path=".", show_hidden=False, dirs_first=True)
file_tree = FileTree(state).on_file_select(lambda path: print(path))
```

## Overlay Widget

### Modal
Modal dialog overlay.

```python
from castella import Modal, ModalState, Column, Button, Text

modal_state = ModalState()
modal = Modal(
    content=Column(
        Text("Modal Content"),
        Button("Close").on_click(lambda _: modal_state.close()),
    ),
    state=modal_state,
    title="My Modal",
)

modal_state.open()   # Open modal
modal_state.close()  # Close modal
```

## Data Display

### DataTable
High-performance data table.

```python
from castella import DataTable, DataTableState, ColumnConfig

state = DataTableState(
    columns=[
        ColumnConfig(name="Name", width=150, sortable=True),
        ColumnConfig(name="Age", width=80, sortable=True),
    ],
    rows=[["Alice", 30], ["Bob", 25]],
)
table = DataTable(state).on_sort(lambda e: print(e.column, e.direction))
```

### Markdown
Rich markdown rendering.

```python
from castella import Markdown

md = Markdown("""
# Heading
**Bold** and *italic*.

```python
print("Hello")
```
""", base_font_size=14, on_link_click=lambda url: print(url))
```

## Common Widget Methods

All widgets support these methods:

```python
widget
    .width(100)
    .height(50)
    .width_policy(SizePolicy.FIXED)
    .height_policy(SizePolicy.EXPANDING)
    .fixed_width(100)
    .fixed_height(50)
    .fixed_size(100, 50)
    .fit_content()
    .fit_parent()
    .bg_color("#ffffff")
    .z_index(10)
    .semantic_id("my-widget")
    .padding(10)
```

```

### references/state.md

```markdown
# Castella State Management

Reactive state patterns for building dynamic UIs.

## State[T]

Observable value wrapper that triggers UI rebuilds:

```python
from castella import State

# Create state
count = State(0)
name = State("Alice")
items = State([1, 2, 3])

# Read value
current = count()

# Set value (triggers rebuild)
count.set(42)

# Operator shortcuts
count += 1    # Increment
count -= 1    # Decrement
count *= 2    # Multiply
count /= 2    # Divide
```

## Attaching State to Components

States must be attached to trigger `view()` rebuilds:

```python
from castella import Component, State

class Counter(Component):
    def __init__(self):
        super().__init__()
        self._count = State(0)
        self._count.attach(self)  # Required!

    def view(self):
        return Text(f"Count: {self._count()}")
```

### Multiple States

Attach each state individually:

```python
class MultiStateComponent(Component):
    def __init__(self):
        super().__init__()
        self._name = State("Alice")
        self._age = State(30)
        # Attach each one
        self._name.attach(self)
        self._age.attach(self)
```

### Alternative: model() for Single State

For single-state components:

```python
class Counter(Component):
    def __init__(self):
        super().__init__()
        self._count = State(0)
        self.model(self._count)  # Shortcut for single state
```

## ListState

Observable list for collections:

```python
from castella import ListState

items = ListState(["a", "b", "c"])

# Mutations (each triggers rebuild)
items.append("d")
items.insert(0, "first")
items.remove("b")
items.pop()
items.clear()

# Iteration
for item in items:
    print(item)

# Length
print(len(items))

# Indexing
print(items[0])
items[0] = "modified"
```

### Atomic Updates with set()

Use `set()` for batch updates (single rebuild):

```python
# BAD: Multiple rebuilds
items.clear()
for item in new_items:
    items.append(item)

# GOOD: Single rebuild
items.set(new_items)
```

### Cached Widget Mapping

Preserve widget instances across rebuilds:

```python
class TodoList(Component):
    def __init__(self):
        super().__init__()
        self._items = ListState([...])
        self._items.attach(self)

    def view(self):
        # Widgets cached by item.id
        widgets = self._items.map_cached(
            lambda item: TodoItemWidget(item.id, item.text)
        )
        return Column(*widgets)
```

Custom key function:

```python
widgets = self._items.map_cached(
    factory=lambda item: MyWidget(item),
    key_fn=lambda item: item.uuid,
)
```

## Component.cache()

Alternative caching on the component:

```python
class MyComponent(Component):
    def view(self):
        # Cache identified by source location
        widgets = self.cache(
            self._items,
            lambda item: TimerWidget(item.id, item.name),
        )
        return Column(*widgets)
```

**When to use which:**
- `ListState.map_cached()` - Simpler API, cache on ListState
- `Component.cache()` - Multiple caches in same view()

## ScrollState

Preserve scroll position across rebuilds:

```python
from castella import ScrollState, Column, SizePolicy

class ScrollableList(Component):
    def __init__(self):
        super().__init__()
        self._items = ListState([...])
        self._items.attach(self)
        self._scroll = ScrollState()  # NOT attached!

    def view(self):
        return Column(
            *[Text(item).fixed_height(30) for item in self._items],
            scrollable=True,
            scroll_state=self._scroll,  # Position preserved
        ).fixed_height(300)
```

### ScrollState Properties

```python
scroll = ScrollState()

scroll.x      # Horizontal scroll position
scroll.y      # Vertical scroll position
scroll.x = 0  # Reset horizontal scroll
scroll.y = 0  # Reset vertical scroll
```

## Input Widget State Pattern

Important: Do NOT attach states managed by Input widgets:

```python
class FormComponent(Component):
    def __init__(self):
        super().__init__()
        self._text = State("initial")
        # DON'T attach - causes focus loss!
        # self._text.attach(self)

    def view(self):
        return Input(self._text()).on_change(
            lambda t: self._text.set(t)
        )
```

Same for MultilineInput:

```python
self._editor = MultilineInputState("content")
# DON'T attach
# self._editor.attach(self)
```

## SliderState

State for Slider widget:

```python
from castella import SliderState

state = SliderState(value=50, min_val=0, max_val=100)

print(state.value())   # 50
print(state.ratio())   # 0.5 (normalized 0-1)
state.set(75)
```

## ProgressBarState

State for ProgressBar widget:

```python
from castella import ProgressBarState

state = ProgressBarState(value=0, min_val=0, max_val=100)
state.set(50)  # Update progress
```

## TabsState

State for Tabs widget:

```python
from castella import TabsState, TabItem

state = TabsState([
    TabItem(id="home", label="Home", content=home_widget),
    TabItem(id="settings", label="Settings", content=settings_widget),
], selected_id="home")

state.select("settings")  # Programmatic selection
```

## ModalState

State for Modal widget:

```python
from castella import ModalState

state = ModalState()
state.open()   # Show modal
state.close()  # Hide modal
```

## TreeState

State for Tree widget:

```python
from castella import TreeState, TreeNode

state = TreeState(nodes, multi_select=False)
state.select("node-id")       # Select node
state.expand_to("node-id")    # Expand to reveal node
```

## State Observation Pattern

States implement observer pattern:

```python
from castella.core import State

count = State(0)

# Add observer
def on_change(new_value):
    print(f"Value changed to: {new_value}")

count.add_observer(on_change)

# Remove observer
count.remove_observer(on_change)
```

## Lazy State Attachment

For components created before App exists:

```python
class MyComponent(Component):
    def __init__(self):
        super().__init__()
        self._state = State(0)
        self._attached = False
        # DON'T attach here

    def view(self):
        # Attach lazily when view() called
        if not self._attached:
            self._state.attach(self)
            self._attached = True
        return Text(str(self._state()))
```

```

castella-core | SkillHub