# 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 */ }
}
```
| text | string | "" | Etiqueta del botón |
| enabled | bool | true | Estado interactivo |
| has-focus | bool | false | Focus ring activo |
| 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;
}
```
| 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
}
```
| title | string | "" |
### NeonSection
```slint
NeonSection {
title: "Components";
collapsible: false;
expanded: true;
@children
}
```
| 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
}
```
| open | bool | false |
| title | string | "" |
| modal-width | length | 480px |
| closable | bool | true |
| 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"; }
}
```
| spacing | length | Theme.sp-3 | Gap entre hijos |
| padding | length | 0px | Padding en todos los lados |
| alignment | string | "stretch" | "start", "center", "end", "stretch" |
### NeonHStack
| 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
| 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 */ }
}
```
| 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. |
| 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. |
| 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 }
}
```
| 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. |
| 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 */ }
}
```
| 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. |
| 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. |
| 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 */ }
}
```
| 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. |
| id | int — clave estable para dismiss. |
| message | string — headline. |
| caption | string — segunda línea opcional. |
| kind | "info" \| "success" \| "warning" \| "danger". |
| 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 */ }
}
```
| 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. |
| 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á.";
}
```
| 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. |
| 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; ... }
}
```
| 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`.
| 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.
| 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();
```
| 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. |
| 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 */ }
}
```
| 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. |
| 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 */ } }
}
```
| 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`.
---