feat: llimphi standalone — framework UI soberano extraído del monorepo

Motor gráfico Llimphi como workspace independiente: bucle Elm
(input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley.
Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets
+ módulos, sin dependencias al resto del monorepo. cargo check --workspace
pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 04:23:42 +00:00
commit e65e9cc623
286 changed files with 46136 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "llimphi-widget-field"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "llimphi-widget-field — wrapper de formulario: label arriba + slot del input + descripción/error abajo. Patrón estándar para formularios accesibles."
[dependencies]
llimphi-ui = { workspace = true }
llimphi-theme = { workspace = true }
+127
View File
@@ -0,0 +1,127 @@
//! `llimphi-widget-field` — wrapper de formulario para inputs.
//!
//! Patrón estándar de campo:
//! ```text
//! Nombre del campo (label — bold, fg_text)
//! [ input control aquí ] (slot — viene como View<Msg>)
//! Descripción o error abajo (helper — fg_muted o fg_destructive)
//! ```
//!
//! El widget no implementa el input — lo recibe como `View<Msg>` y lo
//! envuelve. Esto permite usarlo con `text-input`, `text-area`, `switch`,
//! `segmented` o cualquier otro control.
#![forbid(unsafe_code)]
use llimphi_ui::llimphi_layout::taffy::{
prelude::{length, percent, FlexDirection, Size, Style},
};
use llimphi_ui::llimphi_raster::peniko::Color;
use llimphi_ui::llimphi_text::Alignment;
use llimphi_ui::View;
use llimphi_theme::Theme;
#[derive(Debug, Clone, Copy)]
pub struct FieldPalette {
pub fg_label: Color,
pub fg_helper: Color,
pub fg_error: Color,
pub fg_required: Color,
}
impl FieldPalette {
pub fn from_theme(t: &Theme) -> Self {
Self {
fg_label: t.fg_text,
fg_helper: t.fg_muted,
fg_error: t.fg_destructive,
fg_required: t.fg_destructive,
}
}
}
/// Spec del field. `helper` y `error` son mutuamente excluyentes —
/// si hay error, se renderiza el error (rojo); si no, el helper.
pub struct FieldSpec<Msg: Clone + 'static> {
pub label: String,
/// El input/control concreto (text-input, switch, segmented, etc).
pub control: View<Msg>,
/// Marca el field como requerido — agrega un asterisco al label.
pub required: bool,
/// Texto explicativo debajo del control. `None` para omitirlo.
pub helper: Option<String>,
/// Mensaje de error — gana sobre `helper` cuando está presente.
pub error: Option<String>,
pub palette: FieldPalette,
}
const LABEL_H: f32 = 16.0;
const HELPER_H: f32 = 16.0;
const GAP_LABEL: f32 = 4.0;
const GAP_HELPER: f32 = 4.0;
const LABEL_FONT: f32 = 11.5;
const HELPER_FONT: f32 = 10.5;
pub fn field_view<Msg: Clone + 'static>(spec: FieldSpec<Msg>) -> View<Msg> {
let FieldSpec {
label,
control,
required,
helper,
error,
palette,
} = spec;
// Label: nombre + (asterisco si required).
let label_text = if required { format!("{label} *") } else { label };
let label_view = View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(LABEL_H),
},
flex_shrink: 0.0,
..Default::default()
})
.text_aligned(label_text, LABEL_FONT, palette.fg_label, Alignment::Start);
// Helper / error — error gana si presente.
let helper_text = error.clone().or(helper.clone());
let helper_color = if error.is_some() { palette.fg_error } else { palette.fg_helper };
let mut children: Vec<View<Msg>> = vec![label_view, spacer(GAP_LABEL), control];
if let Some(t) = helper_text {
children.push(spacer(GAP_HELPER));
children.push(
View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(HELPER_H),
},
flex_shrink: 0.0,
..Default::default()
})
.text_aligned(t, HELPER_FONT, helper_color, Alignment::Start),
);
}
View::new(Style {
flex_direction: FlexDirection::Column,
size: Size {
width: percent(1.0_f32),
height: llimphi_ui::llimphi_layout::taffy::prelude::auto(),
},
..Default::default()
})
.children(children)
}
fn spacer<Msg: Clone + 'static>(h: f32) -> View<Msg> {
View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(h),
},
flex_shrink: 0.0,
..Default::default()
})
}