Metadata-Version: 2.4
Name: tupyx
Version: 0.1.1
Summary: A Python library for building reactive web dashboards 100% in Python
Author: GuiEspim18
License-Expression: MIT
Keywords: dashboard,reactive,html,web,bi,charts
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: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Provides-Extra: pandas
Requires-Dist: pandas>=1.5; extra == "pandas"
Provides-Extra: dev
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"

<p align="center">
  <img src="https://raw.githubusercontent.com/GuiEspim18/tupyx/4180cb370a3b50a40a215c4845d7f6e24b9c2ce2/assets/logo.png" width="140" alt="tupyx logo">
</p>

<h1 align="center">tupyx</h1>

<p align="center">
  Framework Python para criar dashboards reativos 100% em Python — sem JavaScript, sem HTML manual, sem templates.
</p>

<p align="center">
  <img src="https://img.shields.io/pypi/v/tupyx" alt="PyPI version">
  <img src="https://img.shields.io/pypi/pyversions/tupyx" alt="Python versions">
  <img src="https://img.shields.io/pypi/l/tupyx" alt="License">
</p>

---

## Instalação

```bash
pip install tupyx
```

Com suporte a pandas:

```bash
pip install tupyx[pandas]
```

---

## Início rápido

```bash
tupyx init meu-dashboard
cd meu-dashboard
tupyx run app.py
```

Abra [http://localhost:8000](http://localhost:8000) no browser. O servidor tem **hot-reload** — salve o arquivo e a página atualiza automaticamente.

---

## Índice

- [Estrutura do projeto](#estrutura-do-projeto)
- [Tags HTML](#tags-html)
- [Estilos com .sx()](#estilos-com-sx)
- [Pseudo-estilos: hover, focus, active](#pseudo-estilos)
- [Eventos](#eventos)
- [Reactive — estado reativo](#reactive)
- [Roteamento com @page](#roteamento-com-page)
- [NavLink — navegação entre rotas](#navlink)
- [Input e Select reativos](#input-e-select-reativos)
- [Medidas com @measure](#medidas-com-measure)
- [Gráficos](#gráficos)
  - [BarChart](#barchart)
  - [LineChart](#linechart)
  - [AreaChart](#areachart)
  - [HorizontalBarChart](#horizontalbarchart)
  - [ScatterChart](#scatterchart)
  - [DonutChart](#donutchart)
  - [PieChart](#piechart)
  - [TableChart](#tablechart)
- [CLI](#cli)

---

## Estrutura do projeto

```
meu-dashboard/
├── app.py          ← ponto de entrada (define o app e chama run)
├── pages/
│   ├── home.py     ← páginas com @page("/rota")
│   └── dash.py
└── pyproject.toml
```

`app.py` mínimo:

```python
from tupyx.ui import *
from tupyx.server import run

def app():
    return Html(
        Head(Title("Meu App")),
        Body(Router()).sx(margin="0", background="#f8fafc"),
    )

run(app)
```

---

## Tags HTML

Todos os elementos HTML estão disponíveis como funções Python:

```python
from tupyx.ui import *

# Uso básico: Tag(filhos, atributos)
Div("conteúdo")
P("Parágrafo de texto")
H1("Título principal")
Span("texto inline")
A("Link", href="https://exemplo.com")
Img(src="/logo.png", alt="Logo")
Button("Clique aqui")

# Aninhamento
Div(
    H1("Título"),
    P("Subtítulo"),
    Button("Ação"),
)

# Atributos HTML
A("Documentação", href="/docs", target="_blank")
Img(src="/foto.jpg", alt="Foto", width="200")
```

**Tags disponíveis:**

| Categoria | Tags |
|---|---|
| Layout | `Div`, `Span`, `P`, `A`, `Img`, `Button`, `Form`, `Label`, `Textarea` |
| Títulos | `H1`, `H2`, `H3`, `H4`, `H5`, `H6` |
| Listas | `Ul`, `Ol`, `Li` |
| Semântica | `Header`, `Footer`, `Main`, `Section`, `Article`, `Nav`, `Aside` |
| Tabelas | `Table`, `Thead`, `Tbody`, `Tfoot`, `Tr`, `Th`, `Td`, `Caption` |
| Mídia | `Video`, `Audio`, `Source`, `Canvas`, `Svg` |
| Documento | `Html`, `Head`, `Body`, `Title`, `Meta`, `Link`, `Script`, `Style` |
| Inline | `Strong`, `Em`, `Small`, `Br`, `Hr`, `Code`, `Pre` |
| Formulário | `Input`, `Select`, `Option` |
| Navegação | `NavLink` |

---

## Estilos com .sx()

`.sx()` aplica CSS inline usando Python `snake_case` (convertido automaticamente para `kebab-case`):

```python
Div("Olá").sx(
    background="#f0f7ff",
    border_radius="12px",
    padding="16px 24px",
    font_size="14px",
    font_weight="600",
    color="#0f172a",
    display="flex",
    align_items="center",
    gap="8px",
    box_shadow="0 2px 8px rgba(0,0,0,0.08)",
)
```

Encadeamento:

```python
Button("Salvar").sx(
    background="#2563eb",
    color="white",
    border="none",
    border_radius="8px",
    padding="10px 20px",
    cursor="pointer",
    font_size="14px",
)
```

---

## Pseudo-estilos

Aplique estilos para `:hover`, `:focus` e `:active`:

```python
Button("Hover aqui").sx(
    background="#2563eb",
    color="white",
    padding="10px 20px",
    border_radius="8px",
    border="none",
    cursor="pointer",
).on_hover(
    background="#1d4ed8",  # escurece no hover
    transform="translateY(-1px)",
)

Input(type="text").sx(
    border="1px solid #dbeafe",
    border_radius="8px",
    padding="8px 12px",
    outline="none",
).on_focus(
    border="1px solid #2563eb",
    box_shadow="0 0 0 3px rgba(37,99,235,0.15)",
)
```

---

## Eventos

Vincule qualquer função Python a eventos do browser:

```python
def home():
    def ao_clicar():
        print("Clicou!")

    def ao_digitar():
        pass  # o estado já é atualizado automaticamente

    return Div(
        Button("Clique").on_click(ao_clicar),
        Button("Duplo clique").on_dblclick(ao_clicar),
        Input(type="text").on_input(ao_digitar),
        Input(type="text").on_change(ao_digitar),
        Div("Hover").on_mouseover(ao_clicar),
    )
```

**Eventos disponíveis:**

| Método | Quando dispara |
|---|---|
| `.on_click(fn)` | clique simples |
| `.on_dblclick(fn)` | duplo clique |
| `.on_mouseover(fn)` | mouse entra |
| `.on_mouseout(fn)` | mouse sai |
| `.on_mouseenter(fn)` | mouse entra (sem propagar para filhos) |
| `.on_mouseleave(fn)` | mouse sai (sem propagar para filhos) |
| `.on_keydown(fn)` | tecla pressionada |
| `.on_keyup(fn)` | tecla solta |
| `.on_input(fn)` | a cada caractere digitado |
| `.on_change(fn)` | valor muda e perde foco |
| `.on_submit(fn)` | formulário enviado |
| `.on_focus_event(fn)` | elemento recebe foco |
| `.on_blur(fn)` | elemento perde foco |
| `.on_js(evento, js)` | JS puro sem passar pelo servidor |

---

## Reactive

`Reactive` cria uma variável de estado que persiste entre renders:

```python
@page("/")
def home():
    contador = Reactive(0)       # valor inicial: 0
    nome     = Reactive("mundo") # string
    itens    = Reactive([])      # lista

    def incrementar():
        contador.update(lambda v: v + 1)

    def zerar():
        contador.set(0)

    return Div(
        H1("Contador: ", contador),       # exibe o valor automaticamente
        P(f"Nome: {nome.get()}"),          # lê o valor
        Button("+1").on_click(incrementar),
        Button("Zerar").on_click(zerar),
    )
```

**Métodos:**

```python
r = Reactive(10)

r.get()                        # → 10
r.set(20)                      # → define 20
r.update(lambda v: v + 5)      # → aplica função ao valor atual
r.subscribe(lambda v: print(v))# → callback chamado em toda atualização
str(r)                         # → "20" (para usar em strings)
r()                            # → 20 (equivale a .get())
```

---

## Roteamento com @page

```python
from tupyx.ui import *
from tupyx.server import run

@page("/", redirect=True)   # redirect=True → redireciona "/" para esta rota
def home():
    return Div(H1("Home"))

@page("/sobre")
def sobre():
    return Div(H1("Sobre"))

@page("/produto/{id}")       # parâmetro dinâmico na URL
def produto(id):
    return Div(H1(f"Produto #{id}"))

def app():
    return Html(
        Head(Title("Meu App")),
        Body(Router()),
    )

run(app)
```

Arquivos em `pages/` são descobertos automaticamente — basta criar o arquivo com `@page(...)`.

---

## NavLink

Navegação entre rotas sem recarregar a página (atualiza apenas o conteúdo):

```python
NavLink("Ir para o dashboard", to="/dashboard")

# Com estilo
NavLink("Dashboard", to="/dashboard").sx(
    color="#2563eb",
    text_decoration="none",
    font_weight="500",
)

# Com aparência de botão
NavLink("Ver relatório", to="/relatorio").sx(
    background="#2563eb",
    color="white",
    padding="8px 16px",
    border_radius="8px",
    text_decoration="none",
    display="inline-block",
)

# Barra de navegação completa
Nav(
    NavLink("Home", to="/").sx(color="#0f172a", text_decoration="none"),
    NavLink("Dashboard", to="/dash").sx(color="#0f172a", text_decoration="none"),
    NavLink("Relatórios", to="/relatorios").sx(color="#0f172a", text_decoration="none"),
).sx(display="flex", gap="24px", padding="16px 32px", border_bottom="1px solid #dbeafe")
```

O botão voltar/avançar do browser funciona corretamente com `NavLink`.

---

## Input e Select reativos

Use `.bind(reactive)` para sincronização bidirecional automática:

### Input

```python
@page("/")
def home():
    nome = Reactive("tupyx")

    return Div(
        Input(type="text", placeholder="Digite seu nome").bind(nome),
        P("Olá, ", nome),  # atualiza automaticamente ao digitar
    )
```

### Select

```python
@page("/")
def home():
    ano = Reactive("2024")

    def filtrado():
        return [d for d in dados if d["ano"] == ano.get()]

    return Div(
        Select(
            Option("2024", value="2024"),
            Option("2023", value="2023"),
            Option("2022", value="2022"),
        ).bind(ano),
        P(f"Mostrando dados de {ano.get()}"),
    )
```

---

## Medidas com @measure

Transforma funções em KPI cards reativos:

```python
dados = [
    {"ano": "2024", "val": 320},
    {"ano": "2024", "val": 480},
    {"ano": "2023", "val": 180},
]

@page("/")
def home():
    ano = Reactive("2024")

    def filtrado():
        return [d for d in dados if d["ano"] == ano.get()]

    @measure(label="Total", prefix="R$ ", color="blue")
    def total():
        return sum(d["val"] for d in filtrado())

    @measure(label="Média", prefix="R$ ", color="green")
    def media():
        d = filtrado()
        return round(sum(i["val"] for i in d) / len(d)) if d else 0

    @measure(label="Melhor", suffix=" pts", color="purple")
    def melhor():
        d = filtrado()
        return max(i["val"] for i in d) if d else 0

    return Div(
        Select(
            Option("2024", value="2024"),
            Option("2023", value="2023"),
        ).bind(ano),
        Div(total, media, melhor).sx(display="flex", gap="16px"),
    )
```

**Parâmetros:**

| Parâmetro | Descrição | Padrão |
|---|---|---|
| `label` | título do card | `""` |
| `prefix` | texto antes do valor (ex: `"R$ "`) | `""` |
| `suffix` | texto depois do valor (ex: `" kg"`) | `""` |
| `color` | cor: `"blue"`, `"green"`, `"yellow"`, `"red"`, `"purple"`, `"gray"` | `"blue"` |

Usar a medida em expressões:

```python
total()             # valor numérico puro
BarChart(y=total)   # passar para gráfico
```

---

## Gráficos

Todos os gráficos aceitam:
- **Listas Python** diretamente
- **`@measure`** (MeasureCard)
- **Callables** (funções que retornam dados)
- **`pandas.DataFrame`** e **`pandas.Series`** (requer `pip install tupyx[pandas]`)

Opções comuns via `.sx()`:

```python
chart.sx(width="100%")           # largura relativa
chart.sx(width="600px")          # largura fixa
chart.sx(height="300px")         # altura
chart.sx(hide_tooltip=True)      # desativa tooltip
chart.sx(hide_legend=True)       # desativa legenda
chart.sx(legend_position="top")  # "top", "bottom", "left", "right"
chart.sx(legend_align="start")   # "start", "center", "end"
```

---

### BarChart

Gráfico de barras vertical. Suporta múltiplas séries.

```python
# Série única
BarChart(
    y=[320, 480, 210, 590, 430],
    x=["Jan", "Fev", "Mar", "Abr", "Mai"],
    title="Vendas Mensais",
    subtitle="Ano 2024",
)

# Múltiplas séries
BarChart(
    y=[[320, 480, 210], [180, 260, 310]],
    x=["Jan", "Fev", "Mar"],
    legends=["2024", "2023"],
    title="Comparativo",
).sx(width="100%")

# Com medida reativa
BarChart(y=minha_medida, x=meses(), title="Vendas").sx(width="100%")
```

**Opções específicas via `.sx()`:**

```python
.sx(hide_x=True)          # oculta labels do eixo X
.sx(hide_y=True)          # oculta labels do eixo Y
.sx(hide_grid_y=True)     # oculta linhas de grade
.sx(show_label=True)      # exibe valor em cima de cada barra
.sx(hide_crosshair=True)  # oculta linha vertical de hover
.sx(bar_radius=8)         # border-radius das barras (padrão: 4)
.sx(padding_left=60)      # margem interna esquerda
```

---

### LineChart

Gráfico de linha. Suporta múltiplas séries e área preenchida.

```python
# Série única
LineChart(
    y=[100, 200, 150, 300, 250],
    x=["Jan", "Fev", "Mar", "Abr", "Mai"],
    title="Crescimento",
)

# Múltiplas séries
LineChart(
    y=[[100, 200, 150], [80, 160, 200]],
    x=["Jan", "Fev", "Mar"],
    legends=["Receita", "Custo"],
).sx(width="100%")
```

**Opções específicas via `.sx()`:**

```python
.sx(hide_x=True)           # oculta labels do eixo X
.sx(hide_y=True)           # oculta labels do eixo Y
.sx(hide_points=True)      # oculta os pontos na linha
.sx(hide_crosshair=True)   # oculta crosshair
.sx(show_grid_x=True)      # exibe grade vertical
.sx(hide_grid_y=True)      # oculta grade horizontal
.sx(show_label=True)       # exibe valor em cada ponto
.sx(fill_area=True)        # preenche área abaixo da linha
.sx(label_position="bottom") # posição do label: "top" ou "bottom"
```

---

### AreaChart

Igual ao LineChart mas com área preenchida por padrão. Ideal para tendências.

```python
AreaChart(
    y=[100, 200, 150, 300, 250, 400],
    x=["Jan", "Fev", "Mar", "Abr", "Mai", "Jun"],
    title="Evolução de Receita",
).sx(width="100%")

# Múltiplas séries sobrepostas
AreaChart(
    y=[[100, 200, 150], [80, 160, 130]],
    x=["Jan", "Fev", "Mar"],
    legends=["Receita", "Custo"],
).sx(width="100%")
```

**Opções específicas via `.sx()`:**

```python
.sx(fill_opacity=0.2)   # opacidade da área (padrão: 0.10)
.sx(hide_points=True)   # oculta pontos
.sx(show_label=True)    # exibe valores
.sx(show_grid_x=True)   # grade vertical
```

---

### HorizontalBarChart

Barras horizontais. Ideal para rankings e comparações com labels longos.

```python
HorizontalBarChart(
    y=[480, 390, 310, 260, 180],
    labels=["Produto A", "Produto B", "Produto C", "Produto D", "Produto E"],
    title="Top Produtos",
).sx(width="100%")
```

**Opções específicas via `.sx()`:**

```python
.sx(show_label=True)    # exibe valor ao lado de cada barra
.sx(show_grid=False)    # oculta grade vertical
```

---

### ScatterChart

Gráfico de dispersão (scatter) e bolhas (bubble).

```python
# Scatter simples — lista de (x, y)
ScatterChart(
    data=[(1, 2), (3, 4), (5, 1), (7, 8), (2, 6)],
    title="Dispersão",
)

# Múltiplas séries
ScatterChart(
    data=[
        [(1, 2), (3, 4), (5, 1)],  # série A
        [(2, 5), (4, 2), (6, 7)],  # série B
    ],
    legends=["Grupo A", "Grupo B"],
).sx(width="100%")

# Bubble chart — (x, y, tamanho)
ScatterChart(
    data=[(1, 2, 10), (3, 4, 30), (5, 1, 20), (7, 8, 50)],
    title="Bolhas",
)
```

**Opções específicas via `.sx()`:**

```python
.sx(hide_x=True)        # oculta eixo X
.sx(hide_y=True)        # oculta eixo Y
.sx(hide_grid_x=True)   # oculta grade vertical
.sx(hide_grid_y=True)   # oculta grade horizontal
.sx(point_r=8)          # raio dos pontos (padrão: 5)
.sx(bubble_max=40)      # raio máximo das bolhas (padrão: 28)
.sx(show_labels=True)   # exibe labels nos pontos
```

---

### DonutChart

Gráfico de rosca. Ideal para proporções e partes de um todo.

```python
DonutChart(
    data=[320, 480, 210, 150],
    labels=["Produto A", "Produto B", "Produto C", "Produto D"],
    title="Distribuição de Vendas",
).sx(width="400px")
```

**Opções específicas via `.sx()`:**

```python
.sx(inner_radius=0.4)   # tamanho do buraco (0 = pizza, 1 = só borda)
.sx(show_label=True)    # exibe labels dentro das fatias
.sx(hide_legend=True)   # oculta legenda
```

---

### PieChart

Gráfico de pizza (igual ao DonutChart sem buraco).

```python
PieChart(
    data=[40, 30, 20, 10],
    labels=["Norte", "Sul", "Leste", "Oeste"],
    title="Vendas por Região",
).sx(width="400px")
```

---

### TableChart

Tabela estilo BI com hover, formatação numérica automática e suporte a pandas.

```python
# Lista de dicts — colunas extraídas automaticamente
TableChart(
    data=[
        {"Mês": "Jan", "Vendas": 320, "Meta": 300},
        {"Mês": "Fev", "Vendas": 480, "Meta": 400},
        {"Mês": "Mar", "Vendas": 210, "Meta": 350},
    ],
    title="Resumo Mensal",
).sx(width="100%")

# Lista de listas com colunas explícitas
TableChart(
    data=[[320, 300], [480, 400], [210, 350]],
    columns=["Vendas", "Meta"],
    title="Resultado",
).sx(width="100%")

# Com pandas DataFrame
import pandas as pd
df = pd.read_csv("vendas.csv")
TableChart(data=df, title="Vendas").sx(width="100%")

# Com medida reativa
@measure()
def tabela():
    return [d for d in dados if d["ano"] == ano.get()]

TableChart(data=tabela, title="Tabela Reativa").sx(width="100%")
```

Colunas numéricas são detectadas automaticamente e recebem alinhamento à direita, fonte monoespaçada e formatação com separador de milhar.

---

## CLI

```bash
# Criar novo projeto
tupyx init meu-dashboard

# Iniciar servidor com hot-reload
tupyx run app.py

# Porta customizada (no app.py)
run(app, port=3000)
```

---

## Licença

MIT © [GuiEspim18](https://github.com/GuiEspim18)
