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
+1
View File
@@ -13,6 +13,7 @@ crate-type = ["cdylib", "rlib"]
gioser-canvas-web = { path = "../../modules/gioser/gioser-canvas-web" }
pluma-reader-web = { path = "../../modules/pluma/pluma-reader-web" }
vista-web = { path = "../../modules/vista/vista-web" }
barra-web = { path = "../../modules/barra/barra-web" }
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
js-sys.workspace = true
+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(())
}
+13 -4
View File
@@ -185,7 +185,11 @@ body.deck-active-fuego .deck { --deck-glow: rgba(245, 144, 86, 0.28); }
body.deck-active-agua .deck { --deck-glow: rgba(108, 208, 243, 0.22); }
body.deck-active-tierra .deck { --deck-glow: rgba(212, 152, 115, 0.24); }
/* Strip horizontal con páginas — vista-web traslada esto. */
/* Strip horizontal con páginas — vista-web traslada esto.
touch-action: pan-y declara al browser "yo manejo horizontal, el
vertical (scroll interno de cada página) lo dejas pasar". Sin esto
el navegador móvil se traga el gesto horizontal antes de que JS
pueda capturarlo. */
.deck-strip {
display: flex;
flex-direction: row;
@@ -194,6 +198,13 @@ body.deck-active-tierra .deck { --deck-glow: rgba(212, 152, 115, 0.24); }
transform: translate3d(var(--vista-offset, 0px), 0, 0);
transition: transform 360ms var(--ease-page);
will-change: transform;
touch-action: pan-y;
}
/* Asegurar que TODOS los descendientes del strip hereden el contrato
touch-action — si el toque llega a un párrafo o <a>, el browser
chequea el touch-action del target, no del padre. */
.deck-strip * {
touch-action: pan-y;
}
.deck-strip.vista-dragging,
.deck-strip.vista-instant {
@@ -207,9 +218,7 @@ body.deck-active-tierra .deck { --deck-glow: rgba(212, 152, 115, 0.24); }
position: relative;
overflow-y: auto;
overflow-x: hidden;
/* contenido alineado al pop-snap mismo si la lista de páginas viviera
dentro de scroll nativo; con vista-web esto es informativo nomás. */
scroll-snap-align: start;
touch-action: pan-y;
}
.deck-page[data-element="aire"] { --page-accent: var(--aire); }
.deck-page[data-element="fuego"] { --page-accent: var(--fuego); }