Metadata-Version: 2.4
Name: streamlit-rtr-components
Version: 1.7.0
Summary: Streamlit components: Org Chart, Hierarchical Grid, Timeline e Local Date Input
Author-email: "Artur R. Oliveira" <arturroliveira87@gmail.com>
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: streamlit>=1.61.0
Requires-Dist: python-dateutil>=2.8.2
Provides-Extra: devel
Requires-Dist: build>=1.2; extra == "devel"
Requires-Dist: wheel; extra == "devel"
Requires-Dist: pytest==7.4.0; extra == "devel"
Requires-Dist: playwright==1.48.0; extra == "devel"
Requires-Dist: requests==2.31.0; extra == "devel"
Requires-Dist: pytest-playwright-snapshot==1.0; extra == "devel"
Requires-Dist: pytest-rerunfailures==12.0; extra == "devel"
Dynamic: license-file

<!-- PROJECT: streamlit-rtr-components -->
<!-- BADGES: start -->
<p align="center">
  <!-- Substitua o href se já estiver no PyPI -->
  <a href="https://pypi.org/project/streamlit-rtr-components/"><img alt="PyPI" src="https://img.shields.io/pypi/v/streamlit-rtr-components.svg"></a>
  <img alt="Python" src="https://img.shields.io/pypi/pyversions/streamlit-rtr-components.svg">
  <img alt="Streamlit" src="https://img.shields.io/badge/streamlit-%E2%89%A51.61-FF4B4B">
  <img alt="License" src="https://img.shields.io/badge/license-MIT-green">
</p>
<!-- BADGES: end -->

<h1 align="center">streamlit-rtr-components</h1>
<p align="center">Componentes customizados para <b>Streamlit</b>: organograma, grid hierárquico, timeline e seletor de datas localizado.</p>

<!-- TOC: start -->
## Sumário
- [Visão geral](#visão-geral)
- [Instalação](#instalação)
- [Exemplo rápido](#exemplo-rápido)
- [API](#api)
- [Formato de dados](#formato-de-dados)
- [Estrutura do repositório](#estrutura-do-repositório)
- [Desenvolvimento (Vite + Tailwind v4)](#desenvolvimento-vite--tailwind-v4)
- [Build de produção (frontend)](#build-de-produção-frontend)
- [Empacotamento Python](#empacotamento-python)
- [Solução de problemas](#solução-de-problemas)
- [Changelog](#changelog)
- [Contribuição](#contribuição)
- [Licença](#licença)
<!-- TOC: end -->

## Visão geral
Quatro componentes prontos para uso em Streamlit:
- `st_orgChart` — organograma (árvore hierárquica).
- `st_hierarchicalGrid` — grid hierárquico com colunas configuráveis e suporte a imagens (PNG/SVG via data URL).
- `st_timeline` — timeline horizontal com múltiplas linhas, zoom, hierarquia e eventos de clique.
- `st_local_date_input` — seletor compacto inspirado no `st.date_input` anterior, com idioma do navegador.

> **Stack**: Python (Streamlit) + React/TypeScript (Vite) + Tailwind v4.  
> **Artefatos**: cada frontend gera `frontend/dist` e é empacotado junto ao wheel.

## Instalação
```bash
pip install streamlit-rtr-components
```


<!-- USAGE: start -->

## Exemplo rápido

```python
import streamlit as st
from rtr_componentes import (
    st_hierarchicalGrid,
    st_local_date_input,
    st_orgChart,
    st_timeline,
)

st.title("Demo • RTR Components")

st.subheader("Org Chart")
st_orgChart(
    data={
        "id": "CEO",
        "children": [{"id": "CTO"}, {"id": "CFO"}]
    }
)

st.subheader("Hierarchical Grid")
data = [
    {
        "estrutura": "Presidência",
        "avatar": "data:image/png;base64,....",   # PNG opcional (data URL)
        "quantidade_funcionarios": 2167,
        "salario_total": "R$ 5.239.551,21",
        "salario_medio": "R$ 2.417,88",
        "children": [
            {
                "estrutura": "Assessoria Financeira",
                "avatar": "data:image/svg+xml;base64,....",  # SVG opcional (data URL)
                "quantidade_funcionarios": 1,
                "salario_total": "R$ 5.184,00",
                "salario_medio": "R$ 5.184,00",
                "children": []
            }
        ]
    }
]
columns = [
    {"label": "Estrutura", "field": "estrutura"},
    {"label": "Quantidade Funcionarios", "field": "quantidade_funcionarios"},
    {"label": "Salario Total", "field": "salario_total", "secret": True},
    {"label": "Salario Medio", "field": "salario_medio"}
]

st_hierarchicalGrid(data=data, columns=columns, expanded=True)

periodo = st_local_date_input(
    "Período",
    value=("2026-08-01", "2026-08-20"),
    format="DD/MM/YYYY",
    key="periodo",
)

event = st_timeline(
    rows=[{"id": "101", "label": "Linha 0101"}],
    items=[{
        "id": "viagem_1",
        "row_id": "101",
        "start": "2026-08-15T06:00:00",
        "end": "2026-08-15T07:15:00",
        "label": "Viagem 001",
        "kind": "planned",
    }],
    start="2026-08-15T05:00:00",
    end="2026-08-15T09:00:00",
    zoom="hour",
    timezone="America/Bahia",
    key="timeline_operacional",
)
```

<!-- USAGE: end -->

## API

```python
from rtr_componentes import (
    st_hierarchicalGrid,
    st_local_date_input,
    st_orgChart,
    st_timeline,
)

st_orgChart(
    data: dict | list | None = None,
    key: str | None = None,
    **kwargs
) -> Any


st_hierarchicalGrid(
    data: dict | list | None = None,
    columns: list[dict] | None = None,
    expanded: bool = False,
    key: str | None = None,
    **kwargs
) -> Any

st_local_date_input(
    label,
    value="today",
    min_value=None,
    max_value=None,
    key=None,
    help=None,
    on_change=None,
    args=None,
    kwargs=None,
    *,
    format="DD/MM/YYYY",
    disabled=False,
    label_visibility="visible",
    width="stretch",
    locale=None,
) -> date | tuple[date, ...] | None

st_timeline(
    rows,
    items,
    start,
    end,
    zoom="hour",
    *,
    timezone="UTC",
    locale="pt-BR",
    colors=None,
    show_now=True,
    height=520,
    selected_item_ids=None,
    editing=None,
    auto_window_on_zoom=False,
    key=None,
) -> Any
```

```text
# data
# Org: objeto/array hierárquico com id, children, etc.
# Grid: nós com campos livres e children: [].
# columns (Grid): {"label": str, "field": str, "secret"?: bool, "type"?: "text"|"number"|"currency"|...}.
# expanded (Grid): expande nós por padrão.
```

O retorno dos componentes é o último valor enviado pelo frontend. Para `st_timeline`, o valor inicial é `None`.


<!-- DATA-FORMAT: start -->

## Formato de dados

### Timeline

`rows` preserva a ordem recebida e aceita hierarquia por `children`. Items só podem apontar para linhas folha:

```python
rows = [{
    "id": "garagem_a",
    "label": "Garagem A",
    "children": [{"id": "101", "label": "Linha 0101"}],
}]

items = [{
    "id": "viagem_1",
    "row_id": "101",
    "start": "2026-08-15T06:00:00",
    "end": "2026-08-15T07:15:00",
    "label": "Viagem 001",
    "kind": "planned",
}]
```

Timestamps usam ISO 8601. Valores com `Z` ou offset são instantes absolutos; valores sem offset são interpretados no argumento `timezone`. A normalização acontece no wrapper Python antes do envio ao frontend.

`zoom` aceita `minute`, `hour`, `day`, `week`, `fortnight`, `month`, `bimester`, `quarter`, `semester` ou `year`. Os identificadores da API permanecem em inglês; com o `locale="pt-BR"` padrão, a interface apresenta `Minuto`, `Hora`, `Dia`, `Semana`, `Quinzena`, `Mês`, `Bimestre`, `Trimestre`, `Semestre` e `Ano`. Também são aceitos locales em português ou inglês, como `pt-PT`, `en-US` e `en-GB`.

Todas as escalas usam períodos semiabertos calculados no timezone informado. Semanas começam na segunda-feira; quinzenas são dias 1–15 e 16–fim do mês; bimestres, trimestres, semestres e anos respeitam seus limites calendáricos. Labels e descrições acessíveis são localizados por Luxon/Intl, sem inferir timezone ou locale do navegador.

`start/end` definem o domínio canônico. Os controles locais `Anterior`, `Próxima`, `Agora` e `Ajustar` navegam dentro desse domínio sem alterá-lo nem emitir eventos. `Ajustar` mostra o domínio inteiro sem trocar a granularidade dos rótulos. A partir de `1.7.0`, cliques repetidos são idempotentes e mantêm o modo ativo. A primeira coluna se adapta aos labels visíveis entre 140 e 420 px, preservando ellipsis e o texto completo no hint nativo.

`auto_window_on_zoom=False` preserva a densidade da versão 1.5.0. Quando habilitado, o primeiro mount começa em `start` e mudanças reais de zoom ajustam localmente a duração visível, procurando manter uma quantidade legível de períodos e o centro atual. A opção é best effort para domínios que atingem o limite seguro de 2.000.000 px e não persiste o viewport através de remounts.

```python
st_timeline(
    rows,
    items,
    start,
    end,
    zoom="fortnight",
    auto_window_on_zoom=True,
)
```

Por teclado, a toolbar usa setas e `Home`/`End`; a zona de labels de rows folha mantém um único ponto de Tab e usa setas verticais e `Home`/`End`; a zona de items também mantém um único ponto de Tab e usa setas para navegar. `Enter`/`Space` produzem o mesmo evento do mouse, e tooltips também abrem por foco e fecham com `Escape`.

`selected_item_ids=None` mantém a seleção visual local. Use `[]` para modo controlado sem seleção ou `["viagem_1"]` para tornar o Python a fonte canônica do único item selecionado. Nesse modo, `item_click` apenas comunica a ativação; a aplicação decide se atualiza a seleção no próximo rerun.

```python
event = st_timeline(
    rows,
    items,
    start,
    end,
    selected_item_ids=["viagem_1"],
)
```

```python
colors = {"planned": "#2563EB", "actual": "#16A34A"}
```

Edição temporal é opt-in. `move`, `resize_start` e `resize_end` são capabilities independentes; o evento é uma proposta, e o consumidor deve validar e atualizar seus próprios dados canônicos:

```python
event = st_timeline(
    rows,
    items,
    start,
    end,
    editing={
        "move": True,
        "resize_start": True,
        "resize_end": True,
        "snap": {
            "step": 5,
            "unit": "minute",
        },
    },
)

if event and event["type"] == "item_change_proposed":
    if event["operation"] == "move":
        # Valide old_*/new_* e atualize items antes do próximo rerun.
        pass
    elif event["operation"] == "resize_start":
        # Somente new_start_ms muda.
        pass
    elif event["operation"] == "resize_end":
        # Somente new_end_ms muda.
        pass
```

Move preserva duração e row. Resize preserva a extremidade oposta, aplica duração estrutural mínima de 1 ms e não faz flip. O `snap` opcional usa grade absoluta no Unix epoch e rounding nearest; consulte [CONTRACTS.md](docs/st_timeline/CONTRACTS.md) para unidades, empate e timezone/DST. Não há constraints configuráveis, troca de row, auto-scroll ou edição por teclado.

Ativar um item retorna o payload compacto abaixo. Cada interação recebe um `event_id` novo:

```json
{
  "type": "item_click",
  "event_id": "...",
  "item_id": "viagem_1",
  "row_id": "101"
}
```

Ativar o label de uma row folha retorna `row_click`; labels e disclosures de grupos não emitem evento:

```json
{
  "type": "row_click",
  "event_id": "...",
  "row_id": "101"
}
```

Faça dispatch pelo campo `type`; `row_click` é a única variante abaixo sem `item_id`.

```python
event = st_timeline(...)

if event:
    if event["type"] == "item_click":
        item_id = event["item_id"]
    elif event["type"] == "row_click":
        row_id = event["row_id"]
    elif event["type"] == "item_change_proposed":
        proposed_start_ms = event["new_start_ms"]
        proposed_end_ms = event["new_end_ms"]
```

Veja `example_timeline.py` para um exemplo completo com grupos, items sobrepostos, `planned`, `actual`, zoom e adaptação ao tema do Streamlit.

Documentação avançada do `st_timeline`:

- [Roadmap](docs/st_timeline/ROADMAP.md)
- [Arquitetura](docs/st_timeline/ARCHITECTURE.md)
- [Contratos públicos](docs/st_timeline/CONTRACTS.md)

### Imagens nos componentes existentes

Imagens: envie como data URL

PNG → data:image/png;base64,<...>

SVG → data:image/svg+xml;base64,<...>

Perfomance: prefira miniaturas (ex.: 64–128 px) para reduzir payload.

Grid: garanta que os nomes em columns[].field existam em cada nó.

<!-- DATA-FORMAT: end -->


## Estrutura do repositório

```text
.
├─ st_orgChart/
│  ├─ __init__.py                 # wrapper Python (usa frontend/dist)
│  └─ frontend/                   # Vite/React/TS
├─ st_hierarchicalGrid/
│  ├─ __init__.py
│  └─ frontend/                   # Vite/React/TS
├─ st_timeline/
│  ├─ __init__.py
│  └─ frontend/                   # Vite/React/TS
├─ st_local_date_input/
│  └─ __init__.py                 # Custom Component v2 inline e isolado
├─ rtr_componentes/
│  └─ __init__.py                 # reexporta os quatro componentes
├─ benchmarks/                    # ferramenta de desenvolvimento; fora do wheel
├─ example_local_date_input.py
├─ example_timeline.py
├─ pyproject.toml
├─ MANIFEST.in
├─ README.md
└─ LICENSE
```

## Desenvolvimento (Vite + Tailwind v4)

Em cada frontend:

```bash
npm i
npm run dev
```

No app Streamlit, ative o modo de desenvolvimento e informe o host específico:

```powershell
$env:DEV_MODE="true"
$env:FRONTEND_HOST_TIMELINE="http://localhost:5174"
streamlit run example_timeline.py
```

Os componentes existentes usam `FRONTEND_HOST_ORG` e `FRONTEND_HOST_GRID`. Em produção, os wrappers leem `frontend/dist`.


<!-- BUILD-FRONTEND: start -->

## Build de produção (frontend)

```bash
cd st_orgChart/frontend && npm run build
cd ../../st_hierarchicalGrid/frontend && npm run build
cd ../../st_timeline/frontend && npm run build
```

<!-- BUILD-FRONTEND: end --> <!-- PACKAGING: start -->

## Empacotamento Python

O `MANIFEST.in` e o `package-data` do `pyproject.toml` incluem os três diretórios `frontend/dist`.

```text
recursive-include st_orgChart/frontend/dist *
recursive-include st_hierarchicalGrid/frontend/dist *
recursive-include st_timeline/frontend/dist *
include README.md
include LICENSE
```

```bash
python -m build
python -m pip install dist/*.whl
python -m pytest tests
```

<!-- PACKAGING: end --> <!-- TROUBLESHOOT: start -->

## Solução de problemas

- Tela branca/404 em produção: confirme `base: "./"` no Vite e a presença de `frontend/dist` no wheel.
- `RuntimeError: Build não encontrado`: execute `npm run build` no frontend correspondente.
- Timestamp ingênuo ambíguo ou inexistente durante transição DST: envie um offset explícito.
- Erros de JSX/TS: use TypeScript 5, `moduleResolution: "Bundler"` e `jsx: "react-jsx"`.

<!-- TROUBLESHOOT: end -->

## Changelog

### 1.7.0

#### Changed

- rows e seus items visuais usam windowing vertical no cliente quando a altura
  lógica excede três viewports;
- uma viewport de overscan é mantida acima e abaixo da faixa visível;
- alturas variáveis, foco assíncrono, edição ativa e âncora de hierarchy são
  preservados durante a materialização sob demanda.
- cliques repetidos em `Ajustar` mantêm o modo Fit ativo e preservam a
  granularidade calendárica selecionada.

#### Compatibility

- dados, lanes e navegação continuam calculados sobre o modelo completo;
- não há API, evento, rerun, paginação Python ou dependência nova;
- Components v1, timezone, seleção, edição temporal, temas e acessibilidade
  permanecem.

### 1.6.1

#### Changed

- `Ajustar` preserva a escala selecionada e funciona como modo persistente até
  um segundo clique;
- a primeira coluna adapta sua largura aos labels visíveis entre 140 e 420 px,
  mantendo ellipsis para textos maiores.

#### Compatibility

- não há mudança na API Python, nos eventos, na seleção, na edição temporal,
  nas lanes ou na hierarquia;
- rows e items continuam completos; paginação/lazy loading permanece fora
  desta release;
- Components v1 e dependências foram preservados.

### 1.6.0

#### Added

- escala temporal `fortnight`, entre `week` e `month`;
- períodos calendáricos em faixas, com labels localizados e descrições
  acessíveis;
- `auto_window_on_zoom=False`, permitindo ajuste opt-in da janela visível ao
  primeiro mount e a mudanças reais de zoom;
- windowing com overscan restrito aos períodos do header/grid.

#### Changed

- `Ajustar` mantém os formatos calendáricos dos rótulos quando a granularidade
  adaptativa corresponde ao período selecionado;
- tooltip de eventos organizado em título e linhas separadas para linha,
  início, fim, categoria e detalhes opcionais.

#### Compatibility

- o default preserva a densidade e o comportamento de viewport da versão
  `1.5.0`;
- Components v1, eventos, seleção, edição temporal, lanes e hierarquia foram
  mantidos;
- não foram adicionadas dependências.

### 1.5.0

#### Added

- `st_local_date_input`, com campo mascarado compacto, seleção simples ou por intervalo e calendário localizado por `navigator.language`;
- opção `locale` para fixar explicitamente um idioma, como `pt-BR`;
- sincronização com `st.session_state[key]` e callback `on_change`.
- localização da interface do `st_timeline`, com português `pt-BR` por padrão;
- escalas calendáricas `month`, `bimester`, `quarter`, `semester` e `year`, além das escalas existentes;
- seleção direta da escala temporal, preservando os controles locais e o domínio `start/end`.

#### Compatibility

- o novo componente usa Custom Components v2 e requer Streamlit 1.61.0 ou superior;
- a distribuição passa a declarar Python 3.10 ou superior, alinhada ao Streamlit 1.61.0;
- o comportamento em `st.form`, `bind="query-params"` e `persist_state` ainda não replica o widget nativo.

### 1.4.0

#### Added

- drag horizontal opt-in;
- resize de início e fim;
- snap temporal configurável;
- evento `item_change_proposed`.

#### Behavior

- preview local durante a edição;
- Python permanece como fonte canônica;
- edição é opt-in;
- `pointermove` não emite proposta;
- cancelamentos não emitem alteração.

#### Compatibility

- APIs existentes foram preservadas;
- edição permanece desabilitada por default;
- nenhuma dependência foi adicionada.

### 1.3.0

#### Added

- seleção controlada via `selected_item_ids`;
- evento `row_click` para labels de rows folha;
- infraestrutura reproduzível de benchmark em `benchmarks/`, restrita ao desenvolvimento e à documentação.

#### Changed

- a grid temporal agora é compartilhada entre rows, reduzindo significativamente o DOM em timelines densas;
- a linha Agora passou a ser materializada uma única vez.

#### Compatibility

- `item_click` foi preservado;
- chamadas existentes sem `selected_item_ids` mantêm a seleção local anterior;
- timezone e regras de DST permanecem inalterados;
- nenhuma dependência foi adicionada.

Os resultados comparativos de performance e suas limitações estão documentados em [TL-017](docs/st_timeline/work-items/TL-017.md).

- 1.2.0 — aprimora a navegação temporal com Fit, Anterior/Próxima/Agora e preservação de contexto em zoom/resize; adiciona navegação acessível por teclado, melhorias ARIA, tooltip acessível, contraste aprimorado e reduced motion, sem quebra da API Python.
- 1.1.1 — corrige a infraestrutura de publicação/CI da versão 1.1.0 para Node 20, sem mudanças funcionais no `st_timeline`.
- 1.1.0 — adiciona `st_timeline`.
- 1.0.0 — release inicial com `st_orgChart` e `st_hierarchicalGrid`.

## Contribuição

Issues e PRs são bem-vindos. Antes de abrir PR:

- rode `npm run build` nos três frontends;
- rode `python -m pytest tests`;
- atualize os exemplos e a documentação quando necessário.

## Licença

MIT — veja `LICENSE`.
