refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
Reorganización física de crates/: - core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/ - shared/ (3 crates) se redistribuye en protocol/ e init/ - lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/ Renames de proyectos: - shipote → shuma (runtime de sandboxes) - nouser → akasha (explorador de Mónadas) - yahweh → nahual (motor GPUI, antes ui_engine/) - lapaloma → pineal (data-viz agnóstica) Fraccionamiento UI → core agnóstico: - vista-core (DeckState + snap, 175 LOC, 5 tests verdes) - barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes) - vista-web y barra-web ahora son thin DOM bindings Documentación nueva: - 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat + 10 módulos + apps/ - docs/STATUS.md con cifras reales por proyecto - docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas) - CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets) Automatización: - scripts/reorg.py — script idempotente que: git mv directorios, renombra package names, recomputa path = refs, reescribe imports rust, actualiza workspace Cargo.toml. Soporta --dry-run. - scripts/split-changelog.py — particiona CHANGELOG por componente. Validación: - cargo check --workspace pasa (124 crates + 2 nuevos cores). - 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "nahual-widget-banner"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Yahweh — widget banner: tira horizontal de status (info/success/warning/error). Reusable cross-app para toasts, errores, mensajes informativos."
|
||||
|
||||
[dependencies]
|
||||
gpui = { workspace = true }
|
||||
nahual-theme = { path = "../../libs/theme" }
|
||||
@@ -0,0 +1,207 @@
|
||||
//! `nahual-widget-banner` — tiras horizontales de status.
|
||||
//!
|
||||
//! Cuatro variants con paleta consistente entre apps:
|
||||
//!
|
||||
//! - [`Banner::Info`] — azul tenue, mensajes neutros.
|
||||
//! - [`Banner::Success`] — verde, confirmaciones de op exitosa
|
||||
//! (toasts típicos).
|
||||
//! - [`Banner::Warning`] — amber, llamadas de atención (modales
|
||||
//! de confirmación, condiciones de "por las dudas").
|
||||
//! - [`Banner::Error`] — rojo, errores fatales o de carga.
|
||||
//!
|
||||
//! Diseño: una `Div` GPUI con paddings + colors hardcoded por
|
||||
//! variant. El caller añade niños via el builder de div (`.child(...)`,
|
||||
//! `.flex()`, etc.) para customizar más allá del default.
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use nahual_widget_banner::{banner, Banner};
|
||||
//!
|
||||
//! // Toast simple (success):
|
||||
//! let toast = banner(Banner::Success, "guardado");
|
||||
//!
|
||||
//! // Banner de error con extra child:
|
||||
//! let err = banner(Banner::Error, "no pude leer log").child(
|
||||
//! div().text_size(px(10.)).child("(timeout 3s)")
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use gpui::{div, hsla, prelude::*, px, App, Background, Div, Hsla, Rgba, SharedString};
|
||||
use nahual_theme::Theme;
|
||||
|
||||
/// Severidad / tono del banner. Determina los colores del fondo,
|
||||
/// texto y border (si aplica). El caller no debería mezclar
|
||||
/// kinds en un mismo banner — usar la composición de divs si
|
||||
/// hace falta una vista híbrida.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Banner {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Banner {
|
||||
/// Color de fondo del banner (sin alpha).
|
||||
pub fn bg(self) -> Rgba {
|
||||
match self {
|
||||
Banner::Info => gpui::rgb(0x1d2a3a),
|
||||
Banner::Success => gpui::rgb(0x2d3a2a),
|
||||
Banner::Warning => gpui::rgb(0x4a3a1a),
|
||||
Banner::Error => gpui::rgb(0x4a2020),
|
||||
}
|
||||
}
|
||||
|
||||
/// Color del texto principal del banner.
|
||||
pub fn fg(self) -> Rgba {
|
||||
match self {
|
||||
Banner::Info => gpui::rgb(0xc0d0e0),
|
||||
Banner::Success => gpui::rgb(0xc0e0a0),
|
||||
Banner::Warning => gpui::rgb(0xf0e0a0),
|
||||
Banner::Error => gpui::rgb(0xffd0d0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Construye un banner con el `kind` indicado y `message` como
|
||||
/// texto principal. Devuelve un [`Div`] al que el caller puede
|
||||
/// agregar children, `id`, handlers, etc.
|
||||
///
|
||||
/// Padding y text_size son los defaults estándar del repo
|
||||
/// (`px(12./6.)` en cada axis, `px(11.)` para el texto). Para un
|
||||
/// banner más grande/chico, llamar `.text_size(...)` / `.px(...)`
|
||||
/// sobre el resultado.
|
||||
pub fn banner(kind: Banner, message: impl Into<SharedString>) -> Div {
|
||||
div()
|
||||
.px(px(12.))
|
||||
.py(px(6.))
|
||||
.bg(kind.bg())
|
||||
.text_color(kind.fg())
|
||||
.text_size(px(11.))
|
||||
.child(message.into())
|
||||
}
|
||||
|
||||
/// Variante themed de [`banner`]: deriva colores siguiendo el
|
||||
/// `Theme::global(cx).is_dark` (lightness flip dark ↔ light) +
|
||||
/// hue fijo por kind (verde para Success, amber para Warning,
|
||||
/// rojo para Error). Info usa `theme.bg_panel_alt` + `theme.accent`
|
||||
/// para integrarse al chrome del app.
|
||||
///
|
||||
/// Beneficio sobre [`banner`]: cuando el usuario cambia de theme
|
||||
/// claro a oscuro, los banners ajustan contraste sin esfuerzo.
|
||||
///
|
||||
/// Si la app no instaló un `Theme`, panicea (`Theme::global` lo
|
||||
/// requiere). Para apps sin theme, usar [`banner`] directo.
|
||||
pub fn banner_themed(cx: &App, kind: Banner, message: impl Into<SharedString>) -> Div {
|
||||
let theme = Theme::global(cx);
|
||||
let (bg, fg) = themed_colors(kind, theme);
|
||||
div()
|
||||
.px(px(12.))
|
||||
.py(px(6.))
|
||||
.bg(bg)
|
||||
.text_color(fg)
|
||||
.text_size(px(11.))
|
||||
.child(message.into())
|
||||
}
|
||||
|
||||
/// Deriva el par `(bg, fg)` para un kind dado contra el theme.
|
||||
/// Public para tests + para que los consumers puedan computar el
|
||||
/// par sin construir el div (ej. para custom layouts).
|
||||
pub fn themed_colors(kind: Banner, theme: &Theme) -> (Background, Hsla) {
|
||||
match kind {
|
||||
Banner::Info => (theme.bg_panel_alt.clone(), theme.accent),
|
||||
Banner::Success => derive_pair(120.0 / 360.0, theme.is_dark),
|
||||
Banner::Warning => derive_pair(40.0 / 360.0, theme.is_dark),
|
||||
Banner::Error => derive_pair(0.0 / 360.0, theme.is_dark),
|
||||
}
|
||||
}
|
||||
|
||||
/// Computa `(bg, fg)` para un hue fijo respetando dark/light mode:
|
||||
/// dark → bg low-lightness, fg high-lightness; light → invertido.
|
||||
fn derive_pair(hue: f32, is_dark: bool) -> (Background, Hsla) {
|
||||
let (bg_l, fg_l) = if is_dark { (0.18, 0.85) } else { (0.92, 0.20) };
|
||||
let bg_hsla = hsla(hue, 0.40, bg_l, 1.0);
|
||||
let fg_hsla = hsla(hue, 0.40, fg_l, 1.0);
|
||||
(bg_hsla.into(), fg_hsla)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn each_kind_has_distinct_bg_color() {
|
||||
// Sanity: ningún kind comparte bg con otro. Si emerge una
|
||||
// versión "low-contrast" de algún kind, abrir en otro
|
||||
// variant en vez de re-usar el color.
|
||||
let bgs = [
|
||||
Banner::Info.bg(),
|
||||
Banner::Success.bg(),
|
||||
Banner::Warning.bg(),
|
||||
Banner::Error.bg(),
|
||||
];
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for b in &bgs {
|
||||
assert!(
|
||||
seen.insert((b.r * 1000.0) as u32 + (b.g * 1000.0) as u32 * 1000),
|
||||
"bg colors collision"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_pair_dark_uses_low_bg_and_high_fg() {
|
||||
let (_bg, fg) = derive_pair(0.0, true);
|
||||
// En dark mode, fg lightness es alta para contraste.
|
||||
assert!(
|
||||
fg.l > 0.7,
|
||||
"fg lightness debería ser alta en dark, got {}",
|
||||
fg.l
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_pair_light_uses_high_bg_and_low_fg() {
|
||||
let (_bg, fg) = derive_pair(0.0, false);
|
||||
// En light mode, fg lightness es baja para contraste.
|
||||
assert!(
|
||||
fg.l < 0.3,
|
||||
"fg lightness debería ser baja en light, got {}",
|
||||
fg.l
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_pair_distinguishes_kinds_by_hue() {
|
||||
// Success/Warning/Error tienen hue distinto; bg lightness
|
||||
// sigue al is_dark de igual forma cross-kind. Así verificar
|
||||
// que cambiar el hue cambia bg.h (no la lightness).
|
||||
let (_, fg_success) = derive_pair(120.0 / 360.0, true);
|
||||
let (_, fg_warning) = derive_pair(40.0 / 360.0, true);
|
||||
let (_, fg_error) = derive_pair(0.0, true);
|
||||
assert!(
|
||||
fg_success.h != fg_warning.h,
|
||||
"success y warning deben diferir en hue"
|
||||
);
|
||||
assert!(fg_warning.h != fg_error.h);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_kind_has_distinct_fg_color() {
|
||||
let fgs = [
|
||||
Banner::Info.fg(),
|
||||
Banner::Success.fg(),
|
||||
Banner::Warning.fg(),
|
||||
Banner::Error.fg(),
|
||||
];
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for f in &fgs {
|
||||
assert!(
|
||||
seen.insert((f.r * 1000.0) as u32 + (f.g * 1000.0) as u32 * 1000)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user