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:
sergio
2026-05-19 14:48:34 +00:00
parent 86fb6ae20b
commit 550c98f275
375 changed files with 8512 additions and 7155 deletions
+41
View File
@@ -0,0 +1,41 @@
# modules/barra/ — Taskbar agnóstica (estilo Windows)
**Propósito.** Lista de tareas (cajitas, una por ventana abierta) que
se renderiza dentro de un `<ul>` provisto por el host. Modelo + render
puros viven en `barra-core`; el binding click+DOM en `barra-web`.
## Crates
| crate | tipo | rol |
| ------------- | ---- | --------------------------------------------------------- |
| `barra-core` | lib | `Task` + `render_html(&[Task]) -> String` + sanitizadores |
| `barra-web` | lib | Mount sobre `<ul>` + listener de click + lookup centers |
## Dependencias
- `barra-core`: sin deps.
- `barra-web``barra-core`, `wasm-bindgen`, `web-sys`.
## Contrato HTML
```html
<ul id="taskbar" class="taskbar-list" role="presentation"></ul>
```
Clases generadas (host estiliza):
- `.taskbar-item`, `.taskbar-item.active`, `.taskbar-item-dot`
- atributo `data-task="<id>"` para theming CSS por tarea
## API
```rust
let bar = TaskList::mount(list_el)?;
bar.set_tasks(&[Task::new("aire", "AIRE"),
Task::new("fuego", "FUEGO").active()]);
bar.on_click(|id, cx, cy| { /* center en CSS pixels */ });
```
## Estado
barra-core: 5 tests verdes (sanitize + escape + render). barra-web:
binding mínimo (mount + click + center lookup). LOC ~280.
@@ -0,0 +1,8 @@
[package]
name = "barra-core"
description = "Barra — modelo de taskbar agnóstico: Task + render-to-html + sanitizadores. Sin dependencias web/DOM."
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
+108
View File
@@ -0,0 +1,108 @@
//! Barra core — modelo agnóstico de taskbar.
//!
//! Provee la lista de `Task`, los helpers de sanitización para atributos
//! HTML, y `render_html` puro. El binding DOM vive en `barra-web`.
/// Una tarea (cajita) en la barra.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Task {
pub id: String,
pub label: String,
pub active: bool,
}
impl Task {
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self { id: id.into(), label: label.into(), active: false }
}
pub fn active(mut self) -> Self {
self.active = true;
self
}
}
/// Renderiza un slice de tareas a markup HTML. Sanitiza IDs y escapa
/// labels. La salida es la lista de `<li>` que el host inyecta en su `<ul>`.
pub fn render_html(tasks: &[Task]) -> String {
let mut html = String::new();
for t in tasks {
let id_safe = sanitize_attr(&t.id);
let label_safe = escape_text(&t.label);
let active_cls = if t.active { " active" } else { "" };
html.push_str(&format!(
"<li><button class=\"taskbar-item{active_cls}\" data-task=\"{id_safe}\" type=\"button\">\
<span class=\"taskbar-item-dot\" aria-hidden=\"true\"></span>{label_safe}</button></li>"
));
}
html
}
/// Filtra a `[a-zA-Z0-9_-]` para uso seguro en atributos HTML.
pub fn sanitize_attr(s: &str) -> String {
s.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.collect()
}
/// HTML-escape de texto para insertarlo en posiciones de contenido.
pub fn escape_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&amp;"),
'<' => out.push_str("&lt;"),
'>' => out.push_str("&gt;"),
'"' => out.push_str("&quot;"),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn task_builder_defaults_inactive() {
let t = Task::new("aire", "AIRE");
assert!(!t.active);
assert!(Task::new("f", "F").active().active);
}
#[test]
fn sanitize_attr_strips_unsafe() {
assert_eq!(sanitize_attr("aire"), "aire");
assert_eq!(sanitize_attr("a-b_c"), "a-b_c");
assert_eq!(sanitize_attr("ai<re>"), "aire");
assert_eq!(sanitize_attr("a\"b"), "ab");
}
#[test]
fn escape_text_escapes_html() {
assert_eq!(escape_text("AIRE"), "AIRE");
assert_eq!(escape_text("<script>"), "&lt;script&gt;");
assert_eq!(escape_text("a & b"), "a &amp; b");
}
#[test]
fn render_html_emits_active_class() {
let tasks = [
Task::new("aire", "AIRE"),
Task::new("fuego", "FUEGO").active(),
];
let html = render_html(&tasks);
assert!(html.contains("data-task=\"aire\""));
assert!(html.contains("data-task=\"fuego\""));
assert!(html.contains("taskbar-item active"));
}
#[test]
fn render_html_escapes_label_and_sanitizes_id() {
let tasks = [Task::new("a<b", "x<script>y")];
let html = render_html(&tasks);
assert!(html.contains("data-task=\"ab\""));
assert!(html.contains("x&lt;script&gt;y"));
assert!(!html.contains("<script>"));
}
}
@@ -7,6 +7,7 @@ authors.workspace = true
publish.workspace = true
[dependencies]
barra-core = { path = "../barra-core" }
wasm-bindgen.workspace = true
js-sys.workspace = true
+12 -129
View File
@@ -1,9 +1,7 @@
//! Barra-web — taskbar estilo Windows, agnóstica del dominio.
//!
//! Maneja la lista dinámica de "tareas" (cajitas, una por ventana abierta)
//! dentro de un elemento `<ul>` provisto por el host. El layout del resto
//! de la barra (home button, brand, créditos, dividers, etc.) es
//! responsabilidad del host — el módulo sólo se encarga del LIST + CLICK.
//! Barra-web — binding DOM de la taskbar. Re-exporta `Task` desde
//! `barra-core` y delega el render-to-html al core. Aquí sólo viven el
//! mount sobre un `<ul>`, el listener de click delegado y los lookups
//! de posición (bounding rects) que son intrínsecos al DOM.
//!
//! Contrato HTML mínimo:
//! ```html
@@ -13,55 +11,18 @@
//! Convenciones de clase generadas:
//! - `.taskbar-item` — cada cajita
//! - `.taskbar-item.active` — la cajita visible/foreground
//! - `.taskbar-item-dot` — punto decorativo dentro de la cajita
//! - `.taskbar-item-dot` — punto decorativo
//! - `data-task="<id>"` — identificador único usable por CSS para theming
//! (`.taskbar-item[data-task="aire"] { --task-color: ... }`)
//!
//! El módulo NO inyecta CSS — el host estiliza estas clases.
//!
//! ```rust,ignore
//! let list: HtmlElement = doc.get_element_by_id("my-tasks")?.dyn_into()?;
//! let bar = barra_web::TaskList::mount(list)?;
//! bar.set_tasks(&[
//! Task::new("aire", "AIRE"),
//! Task::new("fuego", "FUEGO").active(),
//! ]);
//! bar.on_click(|id, cx, cy| {
//! // El click cayó en la cajita `id`. (cx, cy) es el centro de la
//! // cajita en CSS pixels — útil como origin de animaciones.
//! });
//! ```
use std::cell::RefCell;
use std::rc::Rc;
pub use barra_core::Task;
use barra_core::{render_html, sanitize_attr};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Element, HtmlElement, MouseEvent};
/// Una tarea (cajita) en la barra.
#[derive(Clone, Debug)]
pub struct Task {
pub id: String,
pub label: String,
pub active: bool,
}
impl Task {
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self {
id: id.into(),
label: label.into(),
active: false,
}
}
pub fn active(mut self) -> Self {
self.active = true;
self
}
}
#[derive(Clone)]
pub struct TaskList {
list: HtmlElement,
@@ -69,24 +30,15 @@ pub struct TaskList {
}
impl TaskList {
/// Monta el módulo sobre el elemento `<ul>` provisto. Instala un único
/// listener de click delegado: cualquier click dentro del list que caiga
/// sobre un `.taskbar-item` dispara `on_click(id, cx, cy)`.
pub fn mount(list: HtmlElement) -> Result<Self, JsValue> {
let on_click: Rc<RefCell<Option<Box<dyn FnMut(&str, f64, f64)>>>> =
Rc::new(RefCell::new(None));
let on_click2 = on_click.clone();
let cb = Closure::<dyn FnMut(MouseEvent)>::new(move |e: MouseEvent| {
let Some(target) = e.target() else { return };
let Ok(target_el): Result<Element, _> = target.dyn_into() else {
return;
};
let Ok(Some(item)) = target_el.closest(".taskbar-item") else {
return;
};
let Some(id) = item.get_attribute("data-task") else {
return;
};
let Ok(target_el): Result<Element, _> = target.dyn_into() else { return };
let Ok(Some(item)) = target_el.closest(".taskbar-item") else { return };
let Some(id) = item.get_attribute("data-task") else { return };
let rect = item.get_bounding_client_rect();
let cx = rect.left() + rect.width() / 2.0;
let cy = rect.top() + rect.height() / 2.0;
@@ -99,91 +51,22 @@ impl TaskList {
Ok(Self { list, on_click })
}
/// Reemplaza el contenido de la lista con las tareas dadas.
/// Los IDs se filtran a `[a-zA-Z0-9_-]` para uso seguro en atributos.
/// Los labels se HTML-escapan.
pub fn set_tasks(&self, tasks: &[Task]) {
let mut html = String::new();
for t in tasks {
let id_safe = sanitize_attr(&t.id);
let label_safe = escape_text(&t.label);
let active_cls = if t.active { " active" } else { "" };
html.push_str(&format!(
"<li><button class=\"taskbar-item{active_cls}\" data-task=\"{id_safe}\" type=\"button\">\
<span class=\"taskbar-item-dot\" aria-hidden=\"true\"></span>{label_safe}</button></li>"
));
}
self.list.set_inner_html(&html);
self.list.set_inner_html(&render_html(tasks));
}
/// Registra (o reemplaza) el callback al click sobre una cajita.
/// El callback recibe `(id, center_x, center_y)` en CSS pixels.
pub fn on_click<F: FnMut(&str, f64, f64) + 'static>(&self, cb: F) {
*self.on_click.borrow_mut() = Some(Box::new(cb));
}
/// Centro en CSS pixels de la cajita con `id` dado, o `None` si no existe.
pub fn task_center(&self, id: &str) -> Option<(f64, f64)> {
let sel = format!(".taskbar-item[data-task=\"{}\"]", sanitize_attr(id));
let el = self.list.query_selector(&sel).ok().flatten()?;
let rect = el.get_bounding_client_rect();
Some((
rect.left() + rect.width() / 2.0,
rect.top() + rect.height() / 2.0,
))
Some((rect.left() + rect.width() / 2.0, rect.top() + rect.height() / 2.0))
}
/// Acceso al elemento `<ul>` host por si el caller quiere modificar
/// styling o ARIA atributos directamente.
pub fn list_el(&self) -> &HtmlElement {
&self.list
}
}
fn sanitize_attr(s: &str) -> String {
s.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.collect()
}
fn escape_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&amp;"),
'<' => out.push_str("&lt;"),
'>' => out.push_str("&gt;"),
'"' => out.push_str("&quot;"),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn task_builder_defaults_inactive() {
let t = Task::new("aire", "AIRE");
assert!(!t.active);
let t2 = Task::new("fuego", "FUEGO").active();
assert!(t2.active);
}
#[test]
fn sanitize_attr_removes_unsafe_chars() {
assert_eq!(sanitize_attr("aire"), "aire");
assert_eq!(sanitize_attr("a-b_c"), "a-b_c");
assert_eq!(sanitize_attr("ai<re>"), "aire");
assert_eq!(sanitize_attr("a\"b"), "ab");
}
#[test]
fn escape_text_escapes_html() {
assert_eq!(escape_text("AIRE"), "AIRE");
assert_eq!(escape_text("<script>"), "&lt;script&gt;");
assert_eq!(escape_text("a & b"), "a &amp; b");
}
}