Metadata-Version: 2.4
Name: clemui
Version: 0.1.4
Summary: A reactive tkinter-based desktop UI library
Author: User
License-Expression: MIT
Project-URL: Source, https://github.com/user/clemui
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: User Interfaces
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# clemui

A reactive tkinter-based desktop UI library for Python. No external dependencies — pure Python + stdlib tkinter.

```bash
pip install clemui
```

## Quick Start

```python
from clemui import App, Signal, Component, VNode, VBox
from clemui.widgets import Label, Button

class Counter(Component):
    def __init__(self):
        super().__init__()
        self.count = Signal(0)

    def render(self):
        return VNode(VBox, spacing=10, children=[
            VNode(Label, text=f"Count: {self.count.value}"),
            VNode(Button, text="+", command=lambda: self.count.set(self.count.value + 1)),
        ])

app = App(title="Counter")
app.mount(Counter())
app.run()
```

## Core Concepts

### Signals

`Signal` is a reactive value container. When its value changes, all bound widgets and subscribed components update automatically.

```python
name = Signal("Alice")
age = Signal(30)

name.value          # "Alice"
name.set("Bob")     # triggers updates everywhere
```

### Components

Components are class-based UI units. Implement `render()` to return a tree of `VNode`s.

```python
class MyComponent(Component):
    def __init__(self):
        super().__init__()
        self.title = Signal("Hello")

    def render(self):
        return VNode(VBox, children=[
            VNode(Label, text=self.title),
        ])

    def did_mount(self):
        # called after the widget tree is created
        pass

    def did_unmount(self):
        # called before the component is destroyed
        pass
```

### VNodes

A `VNode(type, props, children)` describes a widget declaratively:

```python
VNode(Button, text="Click", command=handler, state='disabled')
VNode(HBox, spacing=8, children=[
    VNode(Label, text="Name:"),
    VNode(Entry, text=signal),
])
```

Properties that are `Signal` instances are auto-bound — the widget updates whenever the signal changes.

## Widgets Reference

| Widget | Import | Key Props |
|--------|--------|-----------|
| **Label** | `clemui.widgets.Label` | `text`, `fg`, `font`, `anchor` |
| **Button** | `clemui.widgets.Button` | `text`, `command`, `state`, `bg` |
| **Entry** | `clemui.widgets.Entry` | `text` (Signal), `width`, `font`, `show` |
| **Text** | `clemui.widgets.Text` | `state`, `height`, `width`, `fg`, `bg` (has scrollbar) |
| **Frame** | `clemui.widgets.Frame` | `height`, `width`, `bg`, `name` (sets `_custom_name`) |
| **Canvas** | `clemui.widgets.Canvas` | `image` (PIL.Image), `bg`, `cursor` |
| **Slider** | `clemui.widgets.Slider` | `value` (Signal), `from_`, `to`, `orient`, `showvalue` |
| **Checkbox** | `clemui.widgets.Checkbox` | `text`, `variable` (Signal), `state` |
| **ComboBox** | `clemui.widgets.ComboBox` | `values` (list/Signal), `value` (Signal), `state` |
| **Treeview** | `clemui.widgets.Treeview` | `columns`, `show` |
| **Menu** | `clemui.widgets.Menu` | `tearoff`, `children` (list of VNodes) |

### Text (Multi-line)

The `Text` widget wraps a `tk.Text` with a scrollbar. Use `get_text()` / `set_text()` to access content.

```python
VNode(Text, state='normal', height=8, width=60, wrap='word')
```

## Layout Helpers

```python
from clemui import VBox, HBox, Padding

VBox(spacing=4)           # vertical stack
HBox(spacing=8)           # horizontal stack
Padding(padx=8, pady=4)   # padding wrapper
```

Each accepts `pack` options for tkinter geometry management:

```python
VNode(VBox, spacing=8, pack={'fill': 'both', 'expand': True})
VNode(Label, text="A", pack={'side': 'left', 'fill': 'x'})
```

## Examples

### Counter with Reset

```python
class Counter(Component):
    def __init__(self):
        super().__init__()
        self.count = Signal(0)
        self.step = Signal(1)

    def render(self):
        return VNode(VBox, spacing=8, pack={'padx': 16, 'pady': 16}, children=[
            VNode(Label, text=f"Count: {self.count.value}",
                  font=("Courier New", 18, "bold")),
            VNode(HBox, spacing=4, children=[
                VNode(Button, text="-", command=lambda: self.count.set(self.count.value - self.step.value)),
                VNode(Button, text="Reset", command=lambda: self.count.set(0)),
                VNode(Button, text="+", command=lambda: self.count.set(self.count.value + self.step.value)),
            ]),
            VNode(HBox, spacing=4, children=[
                VNode(Label, text="Step:"),
                VNode(Slider, from_=1, to=10, orient="horizontal",
                      value=self.step, showvalue=True),
            ]),
        ])
```

### Todo List

```python
class TodoApp(Component):
    def __init__(self):
        super().__init__()
        self.items = Signal([])
        self.input = Signal("")

    def render(self):
        return VNode(VBox, spacing=6, pack={'padx': 12, 'pady': 12}, children=[
            VNode(Label, text="TODO", font=("Courier New", 14, "bold")),
            VNode(HBox, spacing=4, children=[
                VNode(Entry, text=self.input, width=30),
                VNode(Button, text="Add", command=self._add),
            ]),
            VNode(Label, text="\n".join(f"• {i}" for i in self.items.value)
                  if self.items.value else "(empty)",
                  font=("Courier New", 10)),
        ])

    def _add(self):
        if self.input.value.strip():
            self.items.set(self.items.value + [self.input.value.strip()])
            self.input.set("")
```

### Theme Toggle

```python
from clemui.theme import theme

class ThemedApp(Component):
    def render(self):
        m = theme.mode
        return VNode(VBox, children=[
            VNode(Label, text=f"Mode: {m}",
                  fg=theme.primary, font=("Courier New", 12)),
            VNode(Button, text="☀" if m == "dark" else "☾",
                  command=self._toggle),
        ])

    def _toggle(self):
        theme.toggle_mode()
        self._app.root.configure(bg=theme.window_bg)
        self.update()
```

### Form with Dialogs

```python
from clemui import dialogs

class Form(Component):
    def __init__(self):
        super().__init__()
        self.status = Signal("Click a button")

    def render(self):
        return VNode(VBox, spacing=6, children=[
            VNode(Button, text="Open Image", command=lambda: self._open()),
            VNode(Button, text="Save File", command=lambda: self._save()),
            VNode(Label, text=self.status),
        ])

    def _open(self):
        path = dialogs.open_image()
        if path:
            self.status.set(f"Opened: {Path(path).name}")

    def _save(self):
        path = dialogs.save_text(initialfile="out.txt")
        if path:
            Path(path).write_text("hello")
            self.status.set(f"Saved to: {Path(path).name}")
```

Available dialogs:

| Function | Purpose |
|----------|---------|
| `open_image()` | Open image file picker |
| `open_text()` | Open text file picker |
| `save_image(initialfile=...)` | Save image dialog |
| `save_audio(initialfile=...)` | Save audio dialog |
| `save_text(initialfile=...)` | Save text dialog |
| `prompt(title, label, default)` | Simple text input prompt |

### Clock (Reactive Updates)

```python
import time

class Clock(Component):
    def __init__(self):
        super().__init__()
        self.now = Signal("")
        self._tick()

    def render(self):
        return VNode(Label, text=f"🕐 {self.now.value}",
                     font=("Courier New", 24))

    def _tick(self):
        self.now.set(time.strftime("%H:%M:%S"))
        if self._mounted:
            self._app.root.after(1000, self._tick)
```

## Theme System

```python
from clemui.theme import theme

theme.mode           # "dark" or "light"
theme.toggle_mode()  # switch between them
theme.set_mode("light")

# Color tokens (swap between modes)
theme.primary         # button bg, label fg
theme.secondary       # accent
theme.tertiary        # dark red accent
theme.neutral         # window bg, frame bg
theme.grey            # status text, slider trough
```

Dark mode palette:
- Background: `#E9E1D4` (warm beige)
- Text: `#2E2E2E` (dark grey)
- Accents: gold `#B5A642`, red `#8C2727`

## Building Custom Components

```python
class Card(Component):
    def __init__(self, title="", description=""):
        super().__init__()
        self._title = title
        self._desc = description

    def render(self):
        return VNode(Frame, bg=theme.primary, pack={'padx': 6, 'pady': 6}, children=[
            VNode(VBox, spacing=4, children=[
                VNode(Label, text=self._title,
                      font=("Courier New", 12, "bold"), fg=theme.neutral),
                VNode(Label, text=self._desc,
                      font=("Courier New", 10), fg=theme.neutral),
            ]),
        ])

# Usage in a parent component:
VNode(HBox, children=[
    VNode(Card, title="Photo", description="Convert photos"),
    VNode(Card, title="TTS", description="Text to speech"),
])
```

## Tips

- Always call `super().__init__()` in your component's `__init__`
- Use `self._app.root` to access the root tkinter window
- `update()` triggers a full re-render — use it when you need to rebuild the widget tree
- Signals can be passed as widget props for automatic two-way binding
- Use `pack={'fill': 'both', 'expand': True}` on the root VNode to fill the window
- `Component.bind(signal)` subscribes to a signal and calls `update()` on change
