feat(yahweh-widget-card): container card-shape compartido para timeline entries
Iter 3 de integración nakui↔yahweh. El card visual pattern (padding
consistente + rounded + flex_col + gap) que vivía duplicado en cada
timeline entry de nakui-explorer ahora es un widget yahweh reusable.
crates/modules/ui_engine/widgets/card/:
- pub fn card() -> Div: flex_col + px(12) + py(8) + mb(4) +
rounded(4) + gap(2). Sin colores (caller decide via builder).
- 1 test smoke.
nakui-explorer:
- Los 2 timeline entry patterns (Seed/Morphism) pasan de ~7
calls a ~3, intención "card with accent" emerge del nombre.
Tests stack: 111 → 112. App-agnostic — el widget no impone paleta,
permite themes diversos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1be7bf9b1e381ac503609b4d77d385fee6c31ca0 · ruwaq Sergio · 2026-05-10T10:02:23Z (5 killa ñawpaq) · tayta 715cf6be0 · kay commitpi sach'ata qhaway
Semántico sayk'uy · 4
crates/apps/nakui-explorer/src/main.rs
ukhunpi t'ikrakurqan fn render
crates/modules/ui_engine/widgets/card/src/lib.rs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d141805..bf095a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,50 @@ ratio/diff ver `git show <sha>`.
## 2026-05-10
+### feat(yahweh-widget-card): container card-shape compartido para timeline entries
+Iteración 3 de la integración nakui ↔ yahweh. El "card visual"
+pattern (padding consistente + rounded + flex_col + gap) que vivía
+duplicado en cada timeline entry de `nakui-explorer` ahora es un
+widget yahweh reusable. Sin acoplamiento a colores: el caller
+decide bg/border/accent.
+
+Crate nuevo: `crates/modules/ui_engine/widgets/card/`
+(`yahweh-widget-card`):
+- **Dep**: solo `gpui`. App-agnostic.
+- **`pub fn card() -> Div`**: container con `flex_col` + `px(12)`
+ + `py(8)` + `mb(4)` + `rounded(4)` + `gap(2)`. Sin colores
+ aplicados.
+- El return es `Div` GPUI — el caller compone con `.bg(...)`,
+ `.border_l_4()`, `.border_color(...)`, `.child(...)`, hover,
+ on_click, etc., según necesite.
+- 1 test smoke (constructor no panicea).
+
+Migración de `nakui-explorer`:
+- Nueva dep `yahweh-widget-card`.
+- Los 2 patterns de timeline entry (Seed y Morphism) pasan de:
+ ```rust
+ div().flex().flex_col().px(12).py(8).mb(4).bg(card_bg)
+ .rounded(4).border_l_4().border_color(accent).gap(2)...
+ ```
+ a:
+ ```rust
+ card().bg(card_bg).border_l_4().border_color(accent)...
+ ```
+- Reducción ~7 calls → ~3 por entry; legibilidad mejor (la
+ intención "card with accent" emerge del nombre `card()`).
+
+Tests stack: 111 → **112 verdes** (+1 del crate card). Cada crate
+afectado compila y testea individualmente.
+
+Beneficio operativo:
+- Si `MetaApp` o cualquier futura app necesita un container
+ card-shape (ej. info card, expanded list row), `card()` está
+ ya disponible.
+- Cambiar el padding/rounded/gap canónico = un cambio en un solo
+ lugar.
+- El widget no impone colores → no fuerza una paleta y permite
+ themes diversos por app/contexto.
+
### feat(yahweh-widget-banner): widget compartido para toasts/errores cross-app
Patrón visual común a `yahweh-widget-meta-form` (toast success +
error_banner) y `nakui-explorer` (error_banner): un `div` con bg
diff --git a/Cargo.lock b/Cargo.lock
index 797eb7c..e1a9803 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6461,6 +6461,7 @@ dependencies = [
"uuid",
"yahweh-meta-runtime",
"yahweh-widget-banner",
+ "yahweh-widget-card",
]
[[package]]
@@ -13006,6 +13007,13 @@ dependencies = [
"gpui",
]
+[[package]]
+name = "yahweh-widget-card"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+]
+
[[package]]
name = "yahweh-widget-container-core"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 6367f2d..4c0a9f1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -64,6 +64,7 @@ members = [
"crates/modules/ui_engine/widgets/text_input",
"crates/modules/ui_engine/widgets/meta-form",
"crates/modules/ui_engine/widgets/banner",
+ "crates/modules/ui_engine/widgets/card",
# ============================================================
# modules/nakui/ — ERP matemático (nakui absorbido)
diff --git a/crates/apps/nakui-explorer/Cargo.toml b/crates/apps/nakui-explorer/Cargo.toml
index 046a982..e2b394c 100644
--- a/crates/apps/nakui-explorer/Cargo.toml
+++ b/crates/apps/nakui-explorer/Cargo.toml
@@ -9,6 +9,7 @@ description = "Explorador GPUI del event log de Nakui: timeline de seeds + morph
nakui-core = { path = "../../modules/nakui/core" }
yahweh-meta-runtime = { path = "../../modules/ui_engine/libs/meta-runtime" }
yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
+yahweh-widget-card = { path = "../../modules/ui_engine/widgets/card" }
gpui = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true, features = ["serde"] }
diff --git a/crates/apps/nakui-explorer/src/main.rs b/crates/apps/nakui-explorer/src/main.rs
index 51fa042..cfc5fe2 100644
--- a/crates/apps/nakui-explorer/src/main.rs
+++ b/crates/apps/nakui-explorer/src/main.rs
@@ -32,6 +32,7 @@ use gpui::{
use nakui_core::event_log::{EventLog, LogEntry};
use yahweh_meta_runtime::{preview_value, short_hash, short_uuid};
use yahweh_widget_banner::{banner, Banner};
+use yahweh_widget_card::card;
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
@@ -224,17 +225,10 @@ impl Render for Explorer {
.as_ref()
.map(|h| format!("schema={}", short_hash(h)))
.unwrap_or_else(|| "schema=(legacy)".into());
- div()
- .flex()
- .flex_col()
- .px(px(12.))
- .py(px(8.))
- .mb(px(4.))
+ card()
.bg(card_bg)
- .rounded(px(4.))
.border_l_4()
.border_color(accent_seed)
- .gap(px(2.))
.child(
div()
.flex()
@@ -297,17 +291,10 @@ impl Render for Explorer {
.as_ref()
.map(|h| format!("schema={}", short_hash(h)))
.unwrap_or_else(|| "schema=(legacy)".into());
- div()
- .flex()
- .flex_col()
- .px(px(12.))
- .py(px(8.))
- .mb(px(4.))
+ card()
.bg(card_bg)
- .rounded(px(4.))
.border_l_4()
.border_color(accent_morphism)
- .gap(px(2.))
.child(
div()
.flex()
diff --git a/crates/modules/ui_engine/widgets/card/Cargo.toml b/crates/modules/ui_engine/widgets/card/Cargo.toml
new file mode 100644
index 0000000..e12afc8
--- /dev/null
+++ b/crates/modules/ui_engine/widgets/card/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "yahweh-widget-card"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+description = "Yahweh — widget card: container con padding + rounded + flex_col consistentes para timeline entries, list rows expandidas, info cards. Agnóstico del color (caller decide bg/border)."
+
+[dependencies]
+gpui = { workspace = true }
diff --git a/crates/modules/ui_engine/widgets/card/src/lib.rs b/crates/modules/ui_engine/widgets/card/src/lib.rs
new file mode 100644
index 0000000..6928ed7
--- /dev/null
+++ b/crates/modules/ui_engine/widgets/card/src/lib.rs
@@ -0,0 +1,67 @@
+//! `yahweh-widget-card` — container card-shape para entries de
+//! timeline, info cards y similares.
+//!
+//! Aporta la **forma**: padding consistente (12/8), `rounded(4)`,
+//! `flex_col` con `gap(2)`. NO aporta colores — el caller decide
+//! `bg`, `border_color`, etc. via builder calls. Esto permite que
+//! distintos consumers (timeline con accent por kind, info card
+//! con bg uniforme) compartan la misma proporción visual sin
+//! acoplarse a una paleta fija.
+//!
+//! # Ejemplo
+//!
+//! ```ignore
+//! use yahweh_widget_card::card;
+//! use gpui::{rgb, prelude::*, px};
+//!
+//! // Card con accent border-l (típico timeline entry):
+//! let entry = card()
+//! .bg(rgb(0x1d2128))
+//! .border_l_4()
+//! .border_color(rgb(0x88c0d0))
+//! .child(div().child("header"))
+//! .child(div().child("body"));
+//!
+//! // Card sin border (info card uniforme):
+//! let info = card()
+//! .bg(rgb(0x1d2128))
+//! .child("contenido");
+//! ```
+
+#![forbid(unsafe_code)]
+
+use gpui::{div, prelude::*, px, Div};
+
+/// Container card-shape: `flex_col` con padding `12/8`, `rounded(4)`,
+/// `gap(2)` interno entre children y `mb(4)` para separación
+/// vertical de cards apiladas.
+///
+/// Sin colores aplicados — el caller agrega `.bg(...)`,
+/// `.border_color(...)`, `.border_l_4()`, etc. según necesite.
+///
+/// El return es un `Div` GPUI — todas las builder methods de div
+/// están disponibles (children, hover, on_click, ids, etc.).
+pub fn card() -> Div {
+ div()
+ .flex()
+ .flex_col()
+ .px(px(12.))
+ .py(px(8.))
+ .mb(px(4.))
+ .rounded(px(4.))
+ .gap(px(2.))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// Sanity smoke: el constructor devuelve un Div sin panic. No
+ /// podemos asertar las property de styling sin renderear (que
+ /// requiere TestAppContext + window). Si la signature cambia,
+ /// el código no compila — eso es la real garantía.
+ #[test]
+ fn card_returns_div_without_panic() {
+ let _d = card();
+ }
+}