feat(gioser/web): fix mobile swipe, taskbar agnóstica, trazos zodiacales

Mobile drag fix (vista-web):
- pointermove listener ahora con `AddEventListenerOptions { passive: false }`.
  Sin esto, en navegadores móviles `preventDefault()` es no-op y el browser
  se traga el gesto horizontal como pan/scroll antes de que JS pueda
  detectar la dirección y capturar el pointer.
- CSS: `.deck-strip` y `.deck-strip *` y `.deck-page` con
  `touch-action: pan-y`. El touch-action del target inmediato es lo que
  el browser consulta; sin esto, sobre un <p> dentro del strip el browser
  asume `auto` y reclama horizontal.

Taskbar agnóstica (barra-web):
- Nuevo crate `crates/modules/barra/barra-web` que maneja sólo el LIST
  dinámico de tareas; el resto del layout (home, brand, credits) es del
  host. Misma filosofía que vista-web: separar lo reusable.
- API: Task::new(id, label).active() builder; TaskList::mount(ul) +
  set_tasks/on_click/task_center. Click delegado, callback recibe
  (id, cx, cy) en CSS pixels para origin de animaciones.
- Sanitiza IDs a [a-zA-Z0-9_-] y HTML-escapa labels.
- 3 tests unitarios.
- gioser-web refactoreado para consumir TaskList: sync_taskbar arma
  Vec<Task> y delega; on_click del taskbar dispara minimize/restore_from_tab
  según estado. install_taskbar reducido a sólo home buttons.

Trazos zodiacales (gioser-shaders + canvas-web):
- 12 líneas radiales muy sutiles entre la chacana y el aro principal, una
  por signo, con colores significativos:
    Aries→fuego rojo, Tauro→tierra verde, Géminis→aire amarillo,
    Cáncer→agua plata, Leo→fuego dorado, Virgo→tierra marrón,
    Libra→aire rosa, Escorpio→agua rojo profundo, Sagitario→fuego púrpura,
    Capricornio→tierra verde oscuro, Acuario→aire celeste, Piscis→agua
    verde mar.
- Aries empieza en el norte, giran en sentido horario (rueda zodiacal
  clásica). Banda radial r∈[1.05*L, 0.96*ringR_main], gauss angular
  con σ=0.0042 rad (~0.24° de ancho), multiplier 0.55 → apenas visible.
- Uniform `vec3 u_zodiac[12]` subido como array plano de 36 floats vía
  uniform3fv. Constante ZODIAC_COLORS expuesta en canvas-web por si otros
  callers la quieren.

Workspace verde + 21 tests (geom 6 + palette 4 + physics 3 + pluma-md 5
+ barra-web 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-14 02:42:50 +00:00
parent 62058ab193
commit 7728013012
11 changed files with 370 additions and 53 deletions
+38 -47
View File
@@ -19,6 +19,7 @@
use std::cell::RefCell;
use std::rc::Rc;
use barra_web::{Task, TaskList};
use gioser_canvas_web::{tips, Renderer};
use pluma_reader_web::Reader;
use vista_web::Deck;
@@ -48,6 +49,7 @@ struct DeckState {
struct AppState {
document: Document,
deck: Deck,
taskbar: TaskList,
state: RefCell<DeckState>,
}
@@ -195,22 +197,18 @@ impl AppState {
fn sync_taskbar(&self) {
let s = self.state.borrow();
if let Some(list) = self.document.get_element_by_id("taskbar-list") {
let mut html = String::new();
for e in &s.pages {
let label = e.to_uppercase();
let active = if s.active.as_deref() == Some(e.as_str()) {
" active"
} else {
""
};
html.push_str(&format!(
"<li><button class=\"taskbar-item{active}\" data-task=\"{e}\" type=\"button\">\
<span class=\"taskbar-item-dot\" aria-hidden=\"true\"></span>{label}</button></li>"
));
}
list.set_inner_html(&html);
}
let tasks: Vec<Task> = s
.pages
.iter()
.map(|e| {
let mut t = Task::new(e.clone(), e.to_uppercase());
if s.active.as_deref() == Some(e.as_str()) {
t = t.active();
}
t
})
.collect();
self.taskbar.set_tasks(&tasks);
}
fn ensure_page_dom(&self, element: &str) {
@@ -294,8 +292,7 @@ impl AppState {
}
fn taskbar_item_center(&self, element: &str) -> Option<(f64, f64)> {
let sel = format!(".taskbar-item[data-task=\"{}\"]", element);
self.element_center(&sel)
self.taskbar.task_center(element)
}
fn viewport_height(&self) -> f64 {
@@ -337,9 +334,17 @@ pub fn boot() -> Result<(), JsValue> {
.dyn_into()?;
let deck = Deck::mount(strip_el)?;
// Mount barra-web taskbar (manages the dynamic task list).
let list_el: HtmlElement = document
.get_element_by_id("taskbar-list")
.ok_or_else(|| JsValue::from_str("no #taskbar-list"))?
.dyn_into()?;
let taskbar = TaskList::mount(list_el)?;
let app = Rc::new(AppState {
document: document.clone(),
deck: deck.clone(),
taskbar: taskbar.clone(),
state: RefCell::default(),
});
@@ -351,6 +356,19 @@ pub fn boot() -> Result<(), JsValue> {
});
}
// barra on_click → restore / toggle minimize del app
{
let app2 = app.clone();
taskbar.on_click(move |id, cx, cy| {
let is_active = app2.state.borrow().active.as_deref() == Some(id);
if is_active {
app2.minimize(cx, cy);
} else {
app2.restore_from_tab(id, cx, cy);
}
});
}
install_resize(&window, &canvas, &renderer)?;
install_mouse(&document, &canvas, &renderer)?;
install_canvas_pointer(&canvas, &renderer)?;
@@ -517,8 +535,9 @@ fn center_of_element(el: &Element) -> (f64, f64) {
)
}
/// Home button + brand link (data-home) — toda la lógica de tabs vive en
/// barra-web::TaskList. Acá sólo se instalan los handlers para [data-home].
fn install_taskbar(document: &Document, app: &Rc<AppState>) -> Result<(), JsValue> {
// Home button & brand link comparten data-home — minimizan todo.
let homes = document.query_selector_all("[data-home]")?;
for i in 0..homes.length() {
let Some(node) = homes.item(i) else { continue };
@@ -533,34 +552,6 @@ fn install_taskbar(document: &Document, app: &Rc<AppState>) -> Result<(), JsValu
el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
cb.forget();
}
// Delegación en la lista de tabs.
if let Some(list) = document.get_element_by_id("taskbar-list") {
let app2 = app.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;
};
if let Some(task) = item.get_attribute("data-task") {
let rect = item.get_bounding_client_rect();
let cx = rect.left() + rect.width() / 2.0;
let cy = rect.top() + rect.height() / 2.0;
// Si la pestaña ya está activa, minimiza (toggle estilo Windows).
let is_active = app2.state.borrow().active.as_deref() == Some(&task);
if is_active {
app2.minimize(cx, cy);
} else {
app2.restore_from_tab(&task, cx, cy);
}
}
});
list.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
cb.forget();
}
Ok(())
}