tawasuyu

tawasuyu/nahual

Open-with viewers (text/image/audio/video/svg/font/hex/…) in Rust, on Llimphi.

Rust 49TOML 23Markdown 19JSON 11062 Sutichaykuna

⎇ f63e78141d856d65edfc06c793a3e00a7eff7bfe 02_ruway/nahual/nahual-card-viewer-llimphi/src/lib.rs Rust · 148 siq'ikuna · 4.5 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat: nahual standalone — visores open-with sobre Llimphi (front-door, git-dep al monorepo) f63e78141 · Sergio ·

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}