⎇ f63e78141d856d65edfc06c793a3e00a7eff7bfe
02_ruway/nahual/nahual-card-viewer-llimphi/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat: nahual standalone — visores open-with sobre Llimphi (front-door, git-dep al monorepo)
1//! `nahual-card-viewer-llimphi` — visor estructurado de Cards.
2//!
3//! Cuarto visor del shell meta-app (tras texto/imagen/video). Una Card
4//! (`shared/card`) es JSON, así que el text viewer la abriría como tal;
5//! pero el `lens` `card` que `shuma-discern` produce sobre su contenido
6//! merece un visor que la **presente** — no el blob crudo. Este crate
7//! lee la Card, extrae los campos salientes (identidad, naturaleza,
8//! payload, supervisión, capacidades, permisos, referencias) y los pinta
9//! como filas legibles.
10//!
11//! Sigue el patrón fino de los otros viewers: la carga vive en
12//! [`load_card`] (sync — una Card es chica), el render en
13//! [`card_viewer_view`]. No conoce el AppBus: el caller pasa el path.
14//!
15//! MVP feo-primero: el cuerpo es un bloque de texto `clave valor` por
16//! línea, no una tabla con layout. Es legible y autocontenido; cuando un
17//! widget de propiedades reusable exista en el elegance kit, se migra.
18
19#![forbid(unsafe_code)]
20
21use std::path::Path;
22
23use card_core::CardKind;
24use llimphi_ui::llimphi_layout::taffy::{
25 prelude::{length, percent, FlexDirection, Size, Style},
26 AlignItems, Rect,
27};
28use llimphi_ui::llimphi_raster::peniko::Color;
29use llimphi_ui::llimphi_text::Alignment;
30use llimphi_ui::View;
31
32// El dominio (parseo + tipos) vive en `nahual-viewer-core`; lo
33// re-exportamos para no romper a los consumidores.
34pub use nahual_viewer_core::card::*;
35
36/// Paleta del visor. Reusa los slots semánticos del tema.
37#[derive(Debug, Clone, Copy)]
38pub struct CardViewerPalette {
39 pub bg: Color,
40 pub fg_text: Color,
41 pub fg_muted: Color,
42 pub fg_error: Color,
43 pub accent: Color,
44}
45
46impl Default for CardViewerPalette {
47 fn default() -> Self {
48 Self::from_theme(&llimphi_theme::Theme::dark())
49 }
50}
51
52impl CardViewerPalette {
53 pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
54 Self {
55 bg: t.bg_app,
56 fg_text: t.fg_text,
57 fg_muted: t.fg_muted,
58 fg_error: t.fg_destructive,
59 accent: t.accent,
60 }
61 }
62}
63
64/// Pinta header (label · naturaleza) + body con las filas de la Card.
65pub fn card_viewer_view<Msg>(
66 state: &CardPreview,
67 path: Option<&Path>,
68 palette: &CardViewerPalette,
69) -> View<Msg>
70where
71 Msg: Clone + 'static,
72{
73 let header_text = match state {
74 CardPreview::Card(c) => {
75 let kind = match c.kind {
76 CardKind::Ente => "ente",
77 CardKind::Data => "data",
78 };
79 format!("card · {} · {kind}", c.label)
80 }
81 _ => match path {
82 Some(p) => format!(
83 "card · {}",
84 p.file_name()
85 .map(|s| s.to_string_lossy().to_string())
86 .unwrap_or_else(|| p.display().to_string())
87 ),
88 None => "(seleccioná una card)".to_string(),
89 },
90 };
91
92 let header = View::new(Style {
93 size: Size {
94 width: percent(1.0_f32),
95 height: length(20.0_f32),
96 },
97 padding: Rect {
98 left: length(12.0_f32),
99 right: length(12.0_f32),
100 top: length(0.0_f32),
101 bottom: length(0.0_f32),
102 },
103 align_items: Some(AlignItems::Center),
104 ..Default::default()
105 })
106 .text_aligned(header_text, 10.0, palette.accent, Alignment::Start);
107
108 let (body_text, body_color) = match state {
109 CardPreview::Empty => ("—".to_string(), palette.fg_muted),
110 CardPreview::Card(c) => (summarize(c), palette.fg_text),
111 CardPreview::Error(e) => (format!("(card inválida: {e})"), palette.fg_error),
112 };
113
114 let body = View::new(Style {
115 flex_grow: 1.0,
116 size: Size {
117 width: percent(1.0_f32),
118 height: percent(1.0_f32),
119 },
120 padding: Rect {
121 left: length(12.0_f32),
122 right: length(12.0_f32),
123 top: length(6.0_f32),
124 bottom: length(12.0_f32),
125 },
126 ..Default::default()
127 })
128 .text_aligned(body_text, 12.0, body_color, Alignment::Start);
129
130 View::new(Style {
131 flex_direction: FlexDirection::Column,
132 flex_grow: 1.0,
133 size: Size {
134 width: percent(1.0_f32),
135 height: percent(1.0_f32),
136 },
137 padding: Rect {
138 left: length(0.0_f32),
139 right: length(0.0_f32),
140 top: length(6.0_f32),
141 bottom: length(0.0_f32),
142 },
143 ..Default::default()
144 })
145 .fill(palette.bg)
146 .clip(true)
147 .children(vec![header, body])
148}