# Neon Design System — Component API Reference

> Documentación oficial de todas las propiedades y callbacks de los componentes del sistema.

---

## Buttons (`ui/components/button.slint`)

### NeonButton
```slint
NeonButton {
    text: "Click me";
    enabled: true;
    has-focus: false;
    clicked => { /* action */ }
}
```

| Propiedad | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| text | string | "" | Etiqueta del botón |
| enabled | bool | true | Estado interactivo |
| has-focus | bool | false | Focus ring activo |

| Callback | Args | Descripción |
|----------|------|-------------|
| clicked | - | Se dispara al hacer clic |

### PrimaryNeonButton
Mismas propiedades que NeonButton. Fondo cyan relleno.

### GhostNeonButton
Mismas propiedades. Sin borde, solo glow en hover.

### DangerNeonButton
Mismas propiedades. Borde rojo para acciones destructivas.

### IconNeonButton
Mismas propiedades. Tamaño fijo 36×36px para iconos.

### ButtonGroup
Contenedor sin propiedades — solo `@children`.

---

## Inputs (`ui/components/input.slint`)

### NeonInput
```slint
NeonInput {
    text: "value";
    placeholder: "Enter name";
    label: "Project";
    hint: "Required";
    enabled: true;
    has-focus: false;
    has-error: false;
}
```

| Propiedad | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| text | string | "" | Valor del campo |
| placeholder | string | "" | Texto de placeholder |
| label | string | "" | Etiqueta superior |
| hint | string | "" | Texto de ayuda inferior |
| enabled | bool | true | Estado interactivo |
| has-focus | bool | false | Focus ring |
| has-error | bool | false | Borde rojo de error |

### NeonSearchInput
Igual que NeonInput. Placeholder por defecto "Search…".

### NeonTextArea
Igual que NeonInput pero con TextEdit multi-línea (min-height 80px).

---

## Cards (`ui/components/card.slint`)

### NeonCard
```slint
NeonCard {
    title: "Info";
    @children
}
```

| Propiedad | Tipo | Default |
|-----------|------|---------|
| title | string | "" |

### NeonSection
```slint
NeonSection {
    title: "Components";
    collapsible: false;
    expanded: true;
    @children
}
```

| Propiedad | Tipo | Default |
|-----------|------|---------|
| title | string | "" |
| collapsible | bool | false |
| expanded | bool | true |

### InfoCard
```slint
InfoCard {
    icon-text: "◈";
    label: "Models";
    value: "1,247";
    accent-color: Theme.neon-cyan;
}
```

---

## Modals (`ui/components/modal.slint`)

### NeonModal
```slint
NeonModal {
    open: true;
    title: "New Project";
    modal-width: 480px;
    closable: true;
    closed => { /* action */ }
    @children
}
```

| Propiedad | Tipo | Default |
|-----------|------|---------|
| open | bool | false |
| title | string | "" |
| modal-width | length | 480px |
| closable | bool | true |

| Callback | Descripción |
|----------|-------------|
| closed | Al cerrar (click × o backdrop) |

### NeonDrawer
```slint
NeonDrawer {
    open: true;
    title: "Notifications";
    side: "right";  // "left" o "right"
    closed => { /* action */ }
    @children
}
```

### NeonConfirmDialog
```slint
NeonConfirmDialog {
    open: true;
    title: "Delete";
    message: "Are you sure?";
    confirm-danger: true;
    confirmed => { /* yes */ }
    cancelled => { /* no */ }
}
```

---

## Toggles (`ui/components/toggle.slint`)

### NeonToggle
```slint
NeonToggle {
    label: "Auto-sync";
    checked: false;
    enabled: true;
    has-focus: false;
    toggled => { /* action */ }
}
```

### NeonCheckbox
Mismas propiedades que NeonToggle. Checkbox cuadrado 16×16px.

### NeonRadioGroup
```slint
NeonRadioGroup {
    items: ["A", "B", "C"];
    selected-index: 0;
    enabled: true;
    index-changed(idx) => { /* selected index */ }
}
```

---

## Sliders (`ui/components/slider.slint`)

### NeonSlider
```slint
NeonSlider {
    label: "Zoom";
    min: 0.0;
    max: 100.0;
    value: 50.0;
    enabled: true;
    changed => { /* new value */ }
}
```

### NeonProgressBar
```slint
NeonProgressBar {
    value: 0.65;
    color: Theme.neon-cyan;
    show-label: true;
}
```

### NeonSpinner
Sin propiedades. Loader animado de 24×24px.

---

## Badges (`ui/components/badge.slint`)

### NeonBadge, NeonTag, CountBadge, StatusDot
Ver archivo fuente para API detallada.

---

## Lists (`ui/components/list.slint`)

### NeonListGroup, NeonDataTable
Ver archivo fuente para API detallada.

---

## Navigation (`ui/components/nav.slint`)

### NeonSidebar, NeonTopBar
Ver archivo fuente para API detallada.

---

## Icons (`ui/components/icons.slint`)

57 componentes icon: `IconHome`, `IconSettings`, `IconBell`, `IconSearch`, `IconLayers`, etc.

```slint
IconHome {
    color: Theme.neon-cyan;
    size: 24px;
}
```

---

## Charts (`ui/components/chart.slint`)

### NeonBarChart, NeonLineChart
Ver archivo fuente para API detallada.

---

## Form (`ui/components/form.slint`)

### NeonSelect
```slint
NeonSelect {
    label: "Structure";
    options: [{ label: "A", value: "a", disabled: false }];
    selected-index: -1;
    enabled: true;
    has-focus: false;
    selected => { /* option selected */ }
}
```

### NeonDatePicker
```slint
NeonDatePicker {
    label: "Inspection";
    year: 2026;
    month: 5;
    selected-day: -1;
    enabled: true;
    day-selected => { /* day picked */ }
}
```

---

## Tree (`ui/components/tree.slint`)

### NeonTreeView
```slint
NeonTreeView {
    items: [{ label: "File", depth: 0, expanded: true, has-children: true, icon: "📁", selected: false }];
    selected-index: -1;
    item-clicked(idx) => { /* clicked item index */ }
}
```

---

## Color Picker (`ui/components/color-picker.slint`)

### NeonColorPicker
Picker HSV completo con slider de 48 segmentos de hue y campo 2D Sat×Val de 10×10 strips.

---

## Tooltip (`ui/components/tooltip.slint`)

### NeonTooltip
```slint
NeonTooltip {
    text: "Help text";
    shown: false;
    offset-x: 0px;
    offset-y: -28px;
}
```

---

## Pagination (`ui/components/pagination.slint`)

### NeonPagination
```slint
NeonPagination {
    total-pages: 12;
    current-page: 1;
    enabled: true;
    page-changed(p) => { /* new page number */ }
}
```

### NeonBreadcrumbs
```slint
NeonBreadcrumbs {
    items: ["Home", "Projects", "Models"];
    enabled: true;
    navigated(idx) => { /* clicked item index */ }
}
```

---

## SplitView (`ui/components/splitview.slint`)

### NeonSplitView
```slint
NeonSplitView {
    split-position: 200px;
    min-left: 100px;
    min-right: 100px;
    @children  // left panel content
}
```

---

## Notification (`ui/components/notification.slint`)

### NeonNotification
```slint
NeonNotification {
    message: "Export complete!";
    accent: Theme.neon-cyan;
    shown: false;
    dismissed => { /* toast closed */ }
}
```

---

## Dropdown (`ui/components/dropdown.slint`)

### NeonDropdown
```slint
NeonDropdown {
    label: "Select action...";
    items: [{ label: "Open", value: "open", disabled: false }];
    selected-index: -1;
    enabled: true;
    selected(idx) => { /* item selected */ }
}
```

---

## Advanced (`ui/components/advanced.slint`)

### NeonAccordion
```slint
NeonAccordion {
    sections: [{ title: "Details", expanded: true }];
    expanded-index: -1;  // -1 = none
}
```

### NeonSteps
```slint
NeonSteps {
    steps: [{ label: "Plan", completed: true, active: false, pending: false }];
    current-step: 1;
}
```

### NeonTabs (standalone)
```slint
NeonTabs {
    tab-titles: ["Tab A", "Tab B"];
    active-tab: 0;
    tab-changed(idx) => { }
    @children  // content for the current tab
}
```

### NeonEmptyState
```slint
NeonEmptyState {
    icon-text: "📊";
    title: "No data yet";
    description: "Import a model.";
    has-action: true;
    action-label: "Import";
    action-clicked => { }
}
```

### NeonSkeleton
```slint
NeonSkeleton {
    skeleton-width: 200px;
    skeleton-height: 14px;
}
```

### NeonProgressCircular
```slint
NeonProgressCircular {
    value: 0.66;
    size: 48px;
    color: Theme.neon-cyan;
}
```

---

## Layout Helpers (`ui/layouts/neon_stack.slint`)

### NeonVStack
```slint
NeonVStack {
    spacing: Theme.sp-4;
    padding: Theme.sp-3;
    alignment: "stretch";
    NeonButton { text: "Item 1"; }
    NeonButton { text: "Item 2"; }
}
```

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| spacing | length | Theme.sp-3 | Gap entre hijos |
| padding | length | 0px | Padding en todos los lados |
| alignment | string | "stretch" | "start", "center", "end", "stretch" |

### NeonHStack
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| justify | string | "start" | "start", "center", "end", "space-between" |

### NeonContainer
```slint
NeonContainer { sm: true; @children }
```
sm=640px, md=768px, lg=1024px, xl=1280px. Propiedades: padding-x, padding-y.

### NeonSpacer
```slint
NeonHStack { IconHome { } NeonSpacer { } IconSettings { } }
```
Se expande como flex: 1.

### NeonDivider
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| orientation | string | "horizontal" | "horizontal", "vertical" |
| thickness | length | 1px | Ancho |
| color | color | Theme.border | Color |

---

## Typography (`ui/components/typography.slint`)
```slint
NeonH1 { text: "Page Title"; }   // 28px bold
NeonH2 { text: "Section"; }       // 22px bold
NeonH3 { text: "Panel"; }         // 18px bold
NeonH4 { text: "Header"; }        // 15px semibold
NeonH5 { text: "Label"; }         // 13px semibold
NeonH6 { text: "LABEL"; }         // 11px semibold + letter-spacing
NeonBody { text: "Body"; }        // 13px regular
NeonBodySmall { text: "Small"; }  // 12px regular
NeonCaption { text: "Hint"; }     // 11px dim
NeonCode { text: "let x;"; }      // 12px mono + bg
NeonMuted { text: "idle"; }       // 12px muted
```
Todos tienen propiedad `text` (string) y `color` (color, default del tema).

---

## Vertical Sidebar with SVG Icons (`ui/components/nav.slint`)

### NeonImageSidebar
Vertical activity-bar with SVG (`@image-url`) icons. Counterpart to `NeonSidebar` for apps that need theme-aware colorization or pixel-perfect SVG rendering instead of unicode glyphs.

```slint
NeonImageSidebar {
    items: [
        { icon: @image-url("icons/layers.svg"), label: "Capas",   badge-count: 0, disabled: false },
        { icon: @image-url("icons/home.svg"),   label: "BIM",     badge-count: 0, disabled: false },
        { icon: @image-url("icons/info.svg"),   label: "Info",    badge-count: 3, disabled: false },
    ];
    selected-index <=> root.active-tab;
    collapsed: true;             // 60px icon-only strip
    icon-size: 18px;
    item-clicked(idx) => { /* dispatch */ }
}
```

| Propiedad | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| items | [NavItemImage] | [] | Filas del sidebar. |
| selected-index | int | -1 | `in-out` — índice activo. |
| collapsed | bool | false | true → 60px (sólo iconos); false → 240px (icono + label). |
| icon-size | length | 18px | Tamaño del Image. Tuneá según tu SVG. |
| row-height | length | 44px | Min-height por fila. |

| `NavItemImage` | Tipo | Notas |
|---|---|---|
| icon | image | `@image-url("…")` — recomendado SVG con `currentColor` para que `colorize` lo tinte por estado. |
| label | string | Visible solo cuando `collapsed: false`. |
| badge-count | int | > 0 → bubble magenta a la derecha. Cap visual a 99. |
| disabled | bool | Greyed + non-clickable. |

| Callback | Args | Descripción |
|----|----|----|
| item-clicked | int | Índice del item picked. |

**Pattern de colorize**: `Image.colorize` se aplica automáticamente — selected → `Theme.neon-cyan`, idle → `Theme.text-muted`. SVGs single-color con `currentColor` ya respetan esto. Para iconos pre-tintados que no querés que se cambien, no uses este componente — usá un `Image { colorize: transparent }` manual (Slint ignora colorize cuando es transparent).

### NeonSidebar (string-icon counterpart)
Mismo concepto pero con icono unicode/emoji. Útil para menús internos que no necesitan SVG.

```slint
NeonSidebar {
    items: [
        { icon: "🏠", label: "Home",     badge-count: 0, disabled: false },
        { icon: "📊", label: "Analytics", badge-count: 12, disabled: false },
    ];
    selected-index <=> root.tab;
    collapsed: false;
}
```

`NavItem { icon: string, label: string, badge-count: int, disabled: bool }`.

---

## Floating Panel (`ui/components/floating_panel.slint`)

### NeonFloatingPanel
Panel flotante draggable + resizable. Posición y tamaño se exponen como `in-out` para que el host los persista.

```slint
NeonFloatingPanel {
    title: "Capa";
    badge: "12";
    panel-x <=> root.my-panel-x;
    panel-y <=> root.my-panel-y;
    panel-width <=> root.my-panel-width;
    panel-height <=> root.my-panel-height;
    bound-w: root.width;
    bound-h: root.height;
    accent-color: Theme.neon-amber;
    closed => { root.my-panel-visible = false; }
    position-changed => { /* persist x/y */ }
    size-changed => { /* persist w/h */ }
    // Body content
    VerticalLayout { padding: 12px; @children }
}
```

| Propiedad | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| panel-x / panel-y | length | 80px | Posición en parent. `in-out`. |
| panel-width / panel-height | length | 480 / 360 px | Tamaño live. `in-out`. |
| title | string | "" | Texto del título. |
| badge | string | "" | Contador opcional al lado del título. |
| min-panel-width / min-panel-height | length | 240 / 160 px | Clamp inferior. |
| max-panel-width / max-panel-height | length | 4000px | Clamp superior. |
| bound-w / bound-h | length | 9999px | BBox que contiene el panel. |
| closable | bool | true | Mostrar X. |
| resizable | bool | true | Mostrar grip de resize. |
| accent-color | color | Theme.neon-cyan | Tinte del título y X hover. |

| Callback | Descripción |
|----------|-------------|
| closed | Click en X. |
| position-changed | Tras cada paso de drag. |
| size-changed | Tras cada paso de resize. |

> ⚠️ Nota: usa `min-panel-width` (no `min-width`) — Slint reserva `min-width`/`max-width`/`min-height`/`max-height` en `Rectangle`.

---

## Context Menu (`ui/components/context_menu.slint`)

### NeonContextMenu
Menú flotante contextual con dismiss por click-fuera + Escape.

```slint
NeonContextMenu {
    items: [
        { label: "Editar",     icon: "✎", shortcut: "Ctrl+E", disabled: false, danger: false, separator: false },
        { label: "Duplicar",   icon: "⎘", shortcut: "",       disabled: false, danger: false, separator: false },
        { label: "",           icon: "",  shortcut: "",       disabled: false, danger: false, separator: true  },
        { label: "Eliminar",   icon: "✕", shortcut: "Del",    disabled: false, danger: true,  separator: false },
    ];
    visible-flag <=> root.ctx-visible;
    anchor-x: root.click-x;
    anchor-y: root.click-y;
    bound-w: root.width;
    bound-h: root.height;
    item-selected(idx) => { /* dispatch */ }
}
```

| Propiedad | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| items | [ContextMenuItem] | [] | Filas del menú. |
| visible-flag | bool | false | Show/hide. `in-out` (lo limpia el dismiss). |
| anchor-x / anchor-y | length | 0 / 0 | Esquina superior-izquierda en parent coords. |
| menu-width | length | 220px | Ancho fijo. |
| bound-w / bound-h | length | 9999px | Para auto-flip cuando se sale. |

| ContextMenuItem | Tipo | Descripción |
|----|----|----|
| label | string | Texto. |
| icon | string | Glyph unicode. Empty oculta la columna. |
| shortcut | string | Hint a la derecha. |
| disabled | bool | Greyed + non-clickable. |
| danger | bool | Texto rojo + hover rojo. |
| separator | bool | Convierte la fila en divisor de 1px. |

| Callback | Args | Descripción |
|----------|------|-------------|
| item-selected | int | Índice del item elegido. |
| dismissed | - | Click-outside o Esc. |

---

## Toast Queue (`ui/components/notification.slint`)

### NeonToastQueue
Stack vertical de toasts; el host maneja la cola de `[ToastEntry]` y el timer de auto-dismiss.

```slint
NeonToastQueue {
    entries: [
        { id: 1, message: "Capa creada", caption: "",         kind: "success" },
        { id: 2, message: "MQTT desconectado", caption: "Reconectando…", kind: "warning" },
    ];
    position: "bottom-right";  // top-left | top-right | bottom-left | bottom-right
    toast-width: 320px;
    dismiss(id) => { /* remove from Vec */ }
}
```

| Propiedad | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| entries | [ToastEntry] | [] | Lista activa. |
| position | string | "bottom-right" | Esquina del stack. |
| toast-width | length | 320px | Ancho de cada entry. |
| stack-gap | length | 8px | Separación entre entries. |
| edge-margin | length | 16px | Margen al borde del parent. |

| ToastEntry | Tipo |
|----|----|
| id | int — clave estable para dismiss. |
| message | string — headline. |
| caption | string — segunda línea opcional. |
| kind | "info" \| "success" \| "warning" \| "danger". |

| Callback | Args | Descripción |
|----------|------|-------------|
| dismiss | int | Click en X de un toast (id). |

### NeonNotification (legacy)
Toast individual. Mantenido para compatibilidad. Para apps nuevas prefiere `NeonToastQueue`.

---

## Splitter (`ui/components/splitview.slint`)

### NeonSplitterHandle / NeonVSplitterHandle
Primitivas de divider arrastrable. El caller arma el HBox / VBox con sus paneles + un handle entre medio.

```slint
in-out property <length> left-w: 240px;
HorizontalLayout {
    spacing: 0px;
    Rectangle { width: root.left-w; /* left content */ }
    NeonSplitterHandle {
        position <=> root.left-w;
        bound-min: 120px;
        bound-max: parent.width - 200px;
        dragged => { /* persist */ }
    }
    Rectangle { horizontal-stretch: 1; /* right content */ }
}
```

| Propiedad | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| position | length | — | `in-out` — el handle muta esto en drag. |
| bound-min | length | 80px | Mínimo permitido. |
| bound-max | length | 4000px | Máximo. Pasa `parent.width - <trailing min>`. |
| thickness | length | 4px | Ancho del divider. |
| idle-color | color | Theme.border | Color en reposo. |
| active-color | color | Theme.neon-cyan | Color en hover/pressed. |

| Callback | Descripción |
|----------|-------------|
| dragged | Tras cada paso de drag exitoso. |

`NeonVSplitterHandle` tiene la misma API pero arrastra eje Y (cursor `row-resize`).

### NeonSplitView (compat)
Envoltorio legacy con un único `@children` slot (panel izquierdo). Mantenido para call-sites existentes — código nuevo debe usar las primitivas de arriba.

---

## Virtual Table (`ui/components/virtual_table.slint`)

### NeonVirtualTable
Tabla con virtualización vía `ListView`. Drop-in replacement para `NeonDataTable` cuando hay >200 filas.

```slint
NeonVirtualTable {
    columns: ["Id", "Nombre", "Tipo", "Área m²"];
    column-widths: [80px, 200px, 120px, 100px];   // opcional; vacío ⇒ stretch igual
    rows: model.rows;     // [RowEntry { cells: [string] }]
    selected-row <=> model.selected-row;
    sort-column: 0;
    sort-asc: true;
    row-height: 32px;
    header-clicked(c) => { /* re-sort */ }
    row-clicked(i)    => { /* select */ }
    row-double-clicked(i) => { /* drill-in */ }
    empty-title: "Sin features";
    empty-hint: "Importá un GeoJSON para verlos acá.";
}
```

| Propiedad | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| columns | [string] | [] | Headers. |
| column-widths | [length] | [] | Anchos explícitos. Vacío ⇒ stretch. |
| rows | [RowEntry] | [] | Datos (reusa el struct de `list.slint`). |
| selected-row | int | -1 | `in-out`. |
| sort-column / sort-asc | int / bool | -1 / true | Indicador visual; el host hace el sort. |
| row-height / header-height | length | 32 / 36 px | Altura fija (ListView las usa para scroll math). |
| empty-title / empty-hint | string | "Sin datos" / "Filtrá o cargá registros…" | Empty state. |

| Callback | Args | Descripción |
|----------|------|-------------|
| header-clicked | int | Header column index. |
| row-clicked | int | Row index. |
| row-double-clicked | int | Para inline-edit / drill-in. |

---

## Form Validation (`ui/components/form_validation.slint`)

Wrappers visuales — la lógica de validación vive en Rust (Slint no puede modelar validators).

### NeonFormField
Wrap para un input + label + error/help.
```slint
NeonFormField {
    label: "Email";
    help: "Te enviaremos confirmación.";
    error: state.email-error;   // host setea string vacío si OK
    required: true;
    touched: state.email-touched;
    NeonInput { text <=> state.email; ... }
}
```

| Propiedad | Tipo | Default |
|----|----|----|
| label | string | "" |
| help | string | "" |
| error | string | "" — empty = no error |
| required | bool | false (muestra `*` rojo) |
| touched | bool | false (errores supresos hasta primer focus-out) |

### NeonFormGroup
Sección con título + descripción + `@children`.

| Propiedad | Tipo |
|----|----|
| title | string |
| description | string |

### NeonFormSummary
Banner rojo con bullets. Visible solo cuando `errors.length > 0`.
```slint
NeonFormSummary {
    intro: "Revisá los siguientes campos antes de continuar:";
    errors: [
        { field: "Email",    message: "Formato inválido" },
        { field: "Password", message: "Mínimo 8 caracteres" },
    ];
}
```
`FormErrorRow { field: string, message: string }`.

### NeonFormSubmit
Botón gateado por `is-valid` + busy spinner.

| Propiedad | Tipo | Default | Descripción |
|----|----|----|----|
| label | string | "Guardar" | Texto idle. |
| busy-label | string | "Guardando…" | Texto durante el async. |
| is-valid | bool | true | Habilitación. |
| busy | bool | false | Spinner overlay + disabled. |
| variant | string | "primary" | "primary" \| "danger" \| "success". |

Callback `submitted` — se dispara solo cuando `is-valid && !busy`.

---

## Color Picker (`ui/components/color-picker.slint`)

### NeonColorPicker — outputs ahora legibles desde Rust
HSV interno + outputs de RGB/hex que el host puede leer directamente.

```slint
NeonColorPicker {
    hue:  state.h; sat: state.s; val: state.v;  // in-out
    changed   => { state.live = self.hex; }
    committed => { /* persist on pointer-up */ }
}
// Rust:
let rgb = (picker.get_red(), picker.get_green(), picker.get_blue());
let hex = picker.get_hex();
```

| Propiedad | Tipo | Default | Descripción |
|----|----|----|----|
| hue | float | 180.0 | 0..360 (HSV). |
| sat / val | float | 0.6 / 0.7 | 0..1. |
| **out** actual-color | color | — | Valor live. |
| **out** red / green / blue | int | — | 0..255 redondeado. |
| **out** hex | string | — | "#rrggbb" lowercase. |
| apply-rgb | bool | false | Caller flip → reverse-computa HSV. |
| set-from-rgb-r / g / b | float | 0..1 | Inputs para reverse-RGB. |

| Callback | Descripción |
|----|----|
| changed | Cada step del drag. |
| committed | Pointer-up — usa este para persistir. |

---

## Charts Extra (`ui/components/charts_extra.slint`)

### NeonSparkline
Línea inline sin ejes (KPI cards / status bar).
```slint
NeonSparkline {
    values: [12, 18, 25, 22, 30, 28, 35];
    max-val: 50;
    line-color: Theme.neon-cyan;
    show-last-dot: true;
}
```

### NeonAreaChart
Línea + fill con grid.
```slint
NeonAreaChart {
    values: [...]; labels: [...];
    line-color: Theme.neon-purple;
    fill-color: Theme.glow-purple;
    chart-height: 160px;
    max-val: 100;
}
```

### NeonScatterChart
Dots x/y descorrelacionados.
```slint
NeonScatterChart {
    points: [{ x: 25.0, y: 80.0, accent: Theme.neon-cyan }, ...];
    max-x: 100; max-y: 100;
    chart-height: 200px;
    show-grid: true;
}
```
`ScatterPoint { x: float, y: float, accent: color }`. `accent.alpha == 0` ⇒ usa `dot-color`.

### NeonPieChart
Slices via `Path`. Caller pre-computa `start-fraction` / `end-fraction` (Slint no puede sumar dentro de `for`).
```slint
NeonPieChart {
    slices: [
        { start-fraction: 0.0, end-fraction: 0.4, accent: Theme.neon-cyan,    label: "A" },
        { start-fraction: 0.4, end-fraction: 0.7, accent: Theme.neon-magenta, label: "B" },
        { start-fraction: 0.7, end-fraction: 1.0, accent: Theme.neon-green,   label: "C" },
    ];
    chart-size: 200px;
    donut-fraction: 0.0;    // 0 = pie sólido; 0.5 = donut; 0.7 = anillo fino
}
```
`PieSlice { start-fraction: float, end-fraction: float, accent: color, label: string }`.

### NeonStackedBar
"Pie linear" — barra horizontal con segmentos. Mismo input que NeonPieChart.
```slint
NeonStackedBar {
    slices: [...];
    bar-height: 20px;
    radius: 10px;
    show-legend: true;
}
```

---

## Combobox (`ui/components/combobox.slint`)

### NeonCombobox
Select con filtro inline (case-insensitive `contains`).
```slint
NeonCombobox {
    label: "Tipo IFC";
    placeholder: "Seleccionar…";
    options: [
        { label: "IfcWall", value: "wall", disabled: false },
        { label: "IfcSlab", value: "slab", disabled: false },
        ...
    ];
    selected-index <=> state.selected;
    allow-clear: true;
    selected(idx) => { /* idx en el array original, no el filtrado */ }
}
```

| Propiedad | Tipo | Default | Descripción |
|----|----|----|----|
| label | string | "" | Etiqueta superior. |
| placeholder | string | "Buscar…" | Display cuando no hay selección. |
| options | [SelectOption] | [] | Reusa el struct de `form.slint`. |
| selected-index | int | -1 | Índice en el array ORIGINAL. `in-out`. |
| filter-text | string | "" | Texto del filtro live. `in-out`. |
| is-open | bool | false | Estado del popup. `in-out`. |
| max-popup-height | length | 240px | Altura tope; el contenido scrollea adentro. |
| allow-clear | bool | true | Mostrar "⊘ Limpiar selección" cuando hay valor. |

| Callback | Args | Descripción |
|----|----|----|
| selected | int | Índice original. -1 cuando se hace clear. |
| opened / dismissed | - | Lifecycle del popup. |

---

## Date Range Picker (`ui/components/date_range_picker.slint`)

### NeonDateRangePicker
Calendar grid 6×7 con range highlight. Click 1 = start, click 2 = end. Clic 3 = reinicia.

```slint
NeonDateRangePicker {
    label: "Período";
    view-year:  2026; view-month:  5;  // mes mostrado (in-out)
    start-year: -1;   start-month: -1; start-day: -1;  // -1 = unset
    end-year:   -1;   end-month:   -1; end-day:   -1;
    range-changed => { if (self.is-valid) { /* persist */ } }
}
```

| Propiedad | Tipo | Default | Descripción |
|----|----|----|----|
| view-year / view-month | int | 2026 / 5 | Mes mostrado (`in-out`, navegable con ← →). |
| start-year / start-month / start-day | int | -1 | Output (-1 = unset). |
| end-year / end-month / end-day | int | -1 | Output. |
| label | string | "" | Etiqueta superior. |
| enabled | bool | true | Estado interactivo. |
| **out** is-valid | bool | — | true cuando ambos extremos seteados y start ≤ end. |

`range-changed` se dispara tras cada click que muta el rango.

---

## Skeleton (`ui/components/skeleton.slint`)

### NeonSkeleton
Bloque shimmer animado. Variants: `rect` / `line` / `circle`.

```slint
NeonSkeleton {
    variant: "rect";        // "rect" | "line" | "circle"
    skeleton-width: 200px;
    skeleton-height: 16px;
    circle-size: 40px;       // ignorado salvo en variant="circle"
    animated: true;
}
```

### NeonSkeletonText
Stack de N líneas con la última al 75 % de ancho (mimicker de paragraph).
```slint
NeonSkeletonText { lines: 3; line-width: 280px; line-spacing: 8px; }
```

### NeonSkeletonGate
Wrapper conectado a estado: muestra placeholder mientras `loading`, sino renderea `@children`.
```slint
NeonSkeletonGate {
    loading: state.loading;
    placeholder-kind: "card";   // "card" | "list" | "metric" | (otro = paragraph)
    placeholder-width: 320px;
    // Real content
    NeonCard { title: "Resumen"; ... }
}
```

> Nota: `@children` está siempre montado con `visible: !loading` para que no pierda estado (focus, scroll) en cada flip de `loading`.

---
