tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definiciones

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/modules/ui_engine/widgets/stat-card/src/lib.rs Rust · 180 líneas · 5.9 KB Historia Quién tocó qué crudo

feat(yahweh-widget-stat-card): promover patrón stat card como widget af7417ce0 · Sergio ·

1//! `yahweh-widget-stat-card` — tarjeta de dashboard con accent.
2//!
3//! Compone:
4//! - **`card_themed(cx)`** del [`yahweh_widget_card`] como contenedor.
5//! - **Border-l-4** con un color de accent que el caller decide
6//!   (verde = OK, rojo = error, etc.).
7//! - **Label** chico arriba en el color del accent.
8//! - **Value** grande (`px(28)`) en el color principal del text.
9//! - **Description** chica en el color tenue.
10//! - **Listing opcional** de items recientes con sub-header
11//!   `"recent (N de TOTAL):"`.
12//!
13//! El patrón emerge en dashboards estilo `minga-explorer` (counts
14//! del repo + sample) y `brahman-broker-explorer` (estado del
15//! probe). Cada consumer aporta sus propios accents semánticos.
16//!
17//! El widget no asume valor numérico — `value` es
18//! `Into<SharedString>`, así que sirve igual para counts (`"3"`),
19//! status text (`"UP / PROVIDER"`) o cualquier label corto.
20//!
21//! # Ejemplo
22//!
23//! ```ignore
24//! use yahweh_widget_stat_card::stat_card;
25//! use gpui::{rgb, Hsla};
26//!
27//! let cell = stat_card(
28//!     cx,
29//!     "Nodos AST",
30//!     "247",
31//!     "fragments parseados del código",
32//!     rgb(0x88c0d0),
33//!     theme.fg_text,
34//!     theme.fg_muted,
35//!     &["abc123  fn_decl".into(), "def456  expr".into()],
36//! );
37//! ```
38
39#![forbid(unsafe_code)]
40
41use gpui::{div, prelude::*, px, App, IntoElement, SharedString};
42use yahweh_widget_card::card_themed;
43
44/// Construye una stat card. Devuelve `impl IntoElement` para que el
45/// caller pueda meterla directo como child de cualquier
46/// `flex_col`/`gap` parent.
47///
48/// Args:
49/// - `cx` — `&App` (acepta `&Context<T>` por deref). El widget lee
50///   el theme global para el bg de la card.
51/// - `label` — header chico, en el color del accent.
52/// - `value` — texto principal, render grande (`px(28)`).
53/// - `description` — texto chico tenue debajo del value.
54/// - `accent` — color del border-l y del label.
55/// - `text` — color principal (para el value).
56/// - `text_dim` — color tenue (para description y sub-header de
57///   recent).
58/// - `recent_items` — slice de strings; si no vacío, se renderea
59///   como sub-listing con header `"recent (N de TOTAL):"`. Cada
60///   item ocupa una linea.
61#[allow(clippy::too_many_arguments)]
62pub fn stat_card(
63    cx: &App,
64    label: &str,
65    value: impl Into<SharedString>,
66    description: &str,
67    accent: gpui::Rgba,
68    text: gpui::Hsla,
69    text_dim: gpui::Hsla,
70    recent_items: &[String],
71) -> impl IntoElement {
72    let value: SharedString = value.into();
73    let total_for_header = recent_items.len();
74
75    let mut card = card_themed(cx)
76        .border_l_4()
77        .border_color(accent)
78        .child(
79            div()
80                .text_color(accent)
81                .text_size(px(11.))
82                .child(SharedString::from(label.to_string())),
83        )
84        .child(
85            div()
86                .text_color(text)
87                .text_size(px(28.))
88                .child(value),
89        )
90        .child(
91            div()
92                .text_color(text_dim)
93                .text_size(px(11.))
94                .child(SharedString::from(description.to_string())),
95        );
96
97    if !recent_items.is_empty() {
98        // Sub-header indicando cuántos items se muestran.
99        // El "TOTAL" es el len del slice porque el caller ya lo
100        // truncó — no tenemos acceso al total original. Si el
101        // caller quiere "5 de 247", debe formatear el label/value
102        // con el total.
103        card = card.child(
104            div()
105                .mt(px(6.))
106                .text_color(text_dim)
107                .text_size(px(10.))
108                .child(SharedString::from(format!("recent ({total_for_header}):"))),
109        );
110        for it in recent_items {
111            card = card.child(
112                div()
113                    .text_color(text)
114                    .text_size(px(11.))
115                    .child(SharedString::from(it.clone())),
116            );
117        }
118    }
119
120    card
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use gpui::TestAppContext;
127    use yahweh_theme::Theme;
128
129    /// Smoke test: el constructor lee el theme global y devuelve un
130    /// IntoElement. Sin TestAppContext no podemos asertar render
131    /// pixels — esto valida wireup + type-check.
132    #[gpui::test]
133    fn stat_card_constructs_with_theme(cx: &mut TestAppContext) {
134        cx.update(|cx| {
135            Theme::install_default(cx);
136            let theme = Theme::global(cx);
137            let _el = stat_card(
138                cx,
139                "Test",
140                "42",
141                "una descripción",
142                gpui::rgb(0x88c0d0),
143                theme.fg_text,
144                theme.fg_muted,
145                &[],
146            );
147        });
148    }
149
150    #[gpui::test]
151    fn stat_card_with_recent_items_works(cx: &mut TestAppContext) {
152        cx.update(|cx| {
153            Theme::install_default(cx);
154            let theme = Theme::global(cx);
155            let _el = stat_card(
156                cx,
157                "Items",
158                "3",
159                "items recientes:",
160                gpui::rgb(0xa3be8c),
161                theme.fg_text,
162                theme.fg_muted,
163                &["a1b2c3  foo".into(), "d4e5f6  bar".into(), "789012  baz".into()],
164            );
165        });
166    }
167
168    #[gpui::test]
169    fn stat_card_value_accepts_string_or_number_repr(cx: &mut TestAppContext) {
170        // Type-check: value es Into<SharedString>. Tanto literal
171        // string como `format!()` deberían funcionar.
172        cx.update(|cx| {
173            Theme::install_default(cx);
174            let theme = Theme::global(cx);
175            let _ = stat_card(cx, "L", "literal", "d", gpui::rgb(0), theme.fg_text, theme.fg_muted, &[]);
176            let _ = stat_card(cx, "L", format!("{}", 42), "d", gpui::rgb(0), theme.fg_text, theme.fg_muted, &[]);
177            let _ = stat_card(cx, "L", "owned".to_string(), "d", gpui::rgb(0), theme.fg_text, theme.fg_muted, &[]);
178        });
179    }
180}