gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definiciones

gioser-web: draggable graph nodes, fixed controls, history.pushState routing

- Graph: nodes are draggable via pointer events. Snap back with
  spring transition on release (cubic-bezier 0.34,1.56,0.64,1)
- Removed hover bounce animation (was distracting)
- Page controls (minimize/close): now fixed position in viewport
  (top-right, z-index 100), not inside the deck-page scroll area.
  Created once in sync_page_controls() on show/hide deck.
  Controls detect active page when data-minimize/close-page is empty.
- Hash routing → history.pushState: URLs are /estudio/aire etc.
  popstate listener handles back/forward. Initial path read on boot.
- Added PointerEvent feature to gioser-graph-web Cargo.toml
- Added History feature to gioser-web Cargo.toml

05f2e54ed1ebace301975819b16b515a23f23e48 · por Sergio · (hace 5 meses) · padre 4c7d716c0 · ver el árbol en este commit

Delta semántico · 30

crates/apps/gioser-web/pkg/gioser_web.d.ts

crates/apps/gioser-web/src/lib.rs

crates/modules/gioser/gioser-graph-web/src/lib.rs

  • cambió por dentro fn render

10 archivos · +247 −132

diff --git a/Cargo.toml b/Cargo.toml
index bce666a..264decb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -248,20 +248,20 @@ members = [
     "crates/modules/barra/barra-web",
 
     # ============================================================
-#    # modules/cosmobiologia/ — Estudio de astrología profesional
-#    # ============================================================
-#    "crates/modules/cosmobiologia/cosmobiologia-card",
-#    "crates/modules/cosmobiologia/cosmobiologia-model",
-#    "crates/modules/cosmobiologia/cosmobiologia-store",
-#    "crates/modules/cosmobiologia/cosmobiologia-render",
-#    "crates/modules/cosmobiologia/cosmobiologia-corpus",
-#    "crates/modules/cosmobiologia/cosmobiologia-engine",
-#    "crates/modules/cosmobiologia/cosmobiologia-modules",
-#    "crates/modules/cosmobiologia/cosmobiologia-theme",
-#    "crates/modules/cosmobiologia/cosmobiologia-canvas",
-#    "crates/modules/cosmobiologia/cosmobiologia-tree",
-#    "crates/modules/cosmobiologia/cosmobiologia-panel",
-#    "crates/modules/cosmobiologia/cosmobiologia-web",
+####    # modules/cosmobiologia/ — Estudio de astrología profesional
+####    # ============================================================
+####    "crates/modules/cosmobiologia/cosmobiologia-card",
+####    "crates/modules/cosmobiologia/cosmobiologia-model",
+####    "crates/modules/cosmobiologia/cosmobiologia-store",
+####    "crates/modules/cosmobiologia/cosmobiologia-render",
+####    "crates/modules/cosmobiologia/cosmobiologia-corpus",
+####    "crates/modules/cosmobiologia/cosmobiologia-engine",
+####    "crates/modules/cosmobiologia/cosmobiologia-modules",
+####    "crates/modules/cosmobiologia/cosmobiologia-theme",
+####    "crates/modules/cosmobiologia/cosmobiologia-canvas",
+####    "crates/modules/cosmobiologia/cosmobiologia-tree",
+####    "crates/modules/cosmobiologia/cosmobiologia-panel",
+####    "crates/modules/cosmobiologia/cosmobiologia-web",
 
     # ============================================================
     # apps/ — Binarios finales que consumen el protocolo
@@ -287,9 +287,9 @@ members = [
     "crates/apps/pineal-stream-demo",
     "crates/apps/pineal-phosphor-demo",
     "crates/apps/pineal-financial-demo",
-#    "crates/apps/cosmobiologia",
-#    "crates/apps/cosmobiologia-cli",
-#    "crates/apps/cosmobiologia-server",
+####    "crates/apps/cosmobiologia",
+####    "crates/apps/cosmobiologia-cli",
+####    "crates/apps/cosmobiologia-server",
     "crates/apps/dominium",
     "crates/apps/fana",
     "crates/apps/agorapura",
diff --git a/crates/apps/gioser-web/Cargo.toml b/crates/apps/gioser-web/Cargo.toml
index 12e1170..465bd98 100644
--- a/crates/apps/gioser-web/Cargo.toml
+++ b/crates/apps/gioser-web/Cargo.toml
@@ -40,4 +40,5 @@ features = [
     "Performance",
     "console",
     "Location",
+    "History",
 ]
diff --git a/crates/apps/gioser-web/pkg/gioser_web.d.ts b/crates/apps/gioser-web/pkg/gioser_web.d.ts
index 6567e62..dfcbe74 100644
--- a/crates/apps/gioser-web/pkg/gioser_web.d.ts
+++ b/crates/apps/gioser-web/pkg/gioser_web.d.ts
@@ -8,14 +8,15 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
 export interface InitOutput {
     readonly memory: WebAssembly.Memory;
     readonly boot: () => void;
-    readonly __wasm_bindgen_func_elem_229: (a: number, b: number, c: number) => void;
-    readonly __wasm_bindgen_func_elem_1437: (a: number, b: number, c: number, d: number) => void;
-    readonly __wasm_bindgen_func_elem_228: (a: number, b: number, c: number) => void;
-    readonly __wasm_bindgen_func_elem_228_3: (a: number, b: number, c: number) => void;
-    readonly __wasm_bindgen_func_elem_510: (a: number, b: number, c: number) => void;
-    readonly __wasm_bindgen_func_elem_620: (a: number, b: number, c: number) => void;
-    readonly __wasm_bindgen_func_elem_302: (a: number, b: number, c: number) => void;
-    readonly __wasm_bindgen_func_elem_303: (a: number, b: number) => void;
+    readonly __wasm_bindgen_func_elem_234: (a: number, b: number, c: number) => void;
+    readonly __wasm_bindgen_func_elem_1460: (a: number, b: number, c: number, d: number) => void;
+    readonly __wasm_bindgen_func_elem_233: (a: number, b: number, c: number) => void;
+    readonly __wasm_bindgen_func_elem_233_3: (a: number, b: number, c: number) => void;
+    readonly __wasm_bindgen_func_elem_515: (a: number, b: number, c: number) => void;
+    readonly __wasm_bindgen_func_elem_635: (a: number, b: number, c: number) => void;
+    readonly __wasm_bindgen_func_elem_515_6: (a: number, b: number, c: number) => void;
+    readonly __wasm_bindgen_func_elem_307: (a: number, b: number, c: number) => void;
+    readonly __wasm_bindgen_func_elem_308: (a: number, b: number) => void;
     readonly __wbindgen_export: (a: number, b: number) => number;
     readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
     readonly __wbindgen_export3: (a: number) => void;
diff --git a/crates/apps/gioser-web/pkg/gioser_web.js b/crates/apps/gioser-web/pkg/gioser_web.js
index 92f1e92..0f65406 100644
--- a/crates/apps/gioser-web/pkg/gioser_web.js
+++ b/crates/apps/gioser-web/pkg/gioser_web.js
@@ -228,13 +228,6 @@ function __wbg_get_imports() {
             const ret = getObject(arg0).getUniformLocation(getObject(arg1), getStringFromWasm0(arg2, arg3));
             return isLikeNone(ret) ? 0 : addHeapObject(ret);
         },
-        __wbg_hash_db43ea0a219f3045: function() { return handleError(function (arg0, arg1) {
-            const ret = getObject(arg1).hash;
-            const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
-            const len1 = WASM_VECTOR_LEN;
-            getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
-            getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
-        }, arguments); },
         __wbg_height_110f80d27112b6b3: function(arg0) {
             const ret = getObject(arg0).height;
             return ret;
@@ -243,6 +236,10 @@ function __wbg_get_imports() {
             const ret = getObject(arg0).height;
             return ret;
         },
+        __wbg_history_4fea091a79b506f0: function() { return handleError(function (arg0) {
+            const ret = getObject(arg0).history;
+            return addHeapObject(ret);
+        }, arguments); },
         __wbg_id_e2fe0a117fc8156c: function(arg0, arg1) {
             const ret = getObject(arg1).id;
             const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
@@ -426,6 +423,13 @@ function __wbg_get_imports() {
             const ret = getObject(arg0).ok;
             return ret;
         },
+        __wbg_pathname_4da19d3e179041e2: function() { return handleError(function (arg0, arg1) {
+            const ret = getObject(arg1).pathname;
+            const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
+            const len1 = WASM_VECTOR_LEN;
+            getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
+            getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
+        }, arguments); },
         __wbg_pointerId_b61ce7aca1eab0cc: function(arg0) {
             const ret = getObject(arg0).pointerId;
             return ret;
@@ -433,6 +437,9 @@ function __wbg_get_imports() {
         __wbg_preventDefault_077a15ca7e97dc5a: function(arg0) {
             getObject(arg0).preventDefault();
         },
+        __wbg_pushState_c8f418e428c76877: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) {
+            getObject(arg0).pushState(getObject(arg1), getStringFromWasm0(arg2, arg3), arg4 === 0 ? undefined : getStringFromWasm0(arg4, arg5));
+        }, arguments); },
         __wbg_querySelectorAll_0981bdbbafa5bf17: function() { return handleError(function (arg0, arg1, arg2) {
             const ret = getObject(arg0).querySelectorAll(getStringFromWasm0(arg1, arg2));
             return addHeapObject(ret);
@@ -478,9 +485,6 @@ function __wbg_get_imports() {
         __wbg_setProperty_ee784b2651f9ff8d: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
             getObject(arg0).setProperty(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
         }, arguments); },
-        __wbg_set_hash_62c4dbdd31cfb200: function() { return handleError(function (arg0, arg1, arg2) {
-            getObject(arg0).hash = getStringFromWasm0(arg1, arg2);
-        }, arguments); },
         __wbg_set_height_bdd58e6b04e88cca: function(arg0, arg1) {
             getObject(arg0).height = arg1 >>> 0;
         },
@@ -592,51 +596,56 @@ function __wbg_get_imports() {
             return ret;
         },
         __wbindgen_cast_0000000000000001: function(arg0, arg1) {
-            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 187, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_1437);
+            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 196, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_1460);
             return addHeapObject(ret);
         },
         __wbindgen_cast_0000000000000002: function(arg0, arg1) {
             // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [F64], shim_idx: 2, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_229);
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_234);
             return addHeapObject(ret);
         },
         __wbindgen_cast_0000000000000003: function(arg0, arg1) {
             // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 6, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_228);
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_233);
             return addHeapObject(ret);
         },
         __wbindgen_cast_0000000000000004: function(arg0, arg1) {
             // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 6, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_228_3);
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_233_3);
             return addHeapObject(ret);
         },
         __wbindgen_cast_0000000000000005: function(arg0, arg1) {
             // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 144, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_510);
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_515);
             return addHeapObject(ret);
         },
         __wbindgen_cast_0000000000000006: function(arg0, arg1) {
-            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 181, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_620);
+            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 190, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_635);
             return addHeapObject(ret);
         },
         __wbindgen_cast_0000000000000007: function(arg0, arg1) {
-            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PointerEvent")], shim_idx: 74, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_302);
+            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PointerEvent")], shim_idx: 144, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_515_6);
             return addHeapObject(ret);
         },
         __wbindgen_cast_0000000000000008: function(arg0, arg1) {
-            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 76, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
-            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_303);
+            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PointerEvent")], shim_idx: 74, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_307);
             return addHeapObject(ret);
         },
         __wbindgen_cast_0000000000000009: function(arg0, arg1) {
+            // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 76, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
+            const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_308);
+            return addHeapObject(ret);
+        },
+        __wbindgen_cast_000000000000000a: function(arg0, arg1) {
             // Cast intrinsic for `Ref(Slice(F32)) -> NamedExternref("Float32Array")`.
             const ret = getArrayF32FromWasm0(arg0, arg1);
             return addHeapObject(ret);
         },
-        __wbindgen_cast_000000000000000a: function(arg0, arg1) {
+        __wbindgen_cast_000000000000000b: function(arg0, arg1) {
             // Cast intrinsic for `Ref(String) -> Externref`.
             const ret = getStringFromWasm0(arg0, arg1);
             return addHeapObject(ret);
@@ -655,34 +664,38 @@ function __wbg_get_imports() {
     };
 }
 
-function __wasm_bindgen_func_elem_303(arg0, arg1) {
-    wasm.__wasm_bindgen_func_elem_303(arg0, arg1);
+function __wasm_bindgen_func_elem_308(arg0, arg1) {
+    wasm.__wasm_bindgen_func_elem_308(arg0, arg1);
+}
+
+function __wasm_bindgen_func_elem_233(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_233(arg0, arg1, addHeapObject(arg2));
 }
 
-function __wasm_bindgen_func_elem_228(arg0, arg1, arg2) {
-    wasm.__wasm_bindgen_func_elem_228(arg0, arg1, addHeapObject(arg2));
+function __wasm_bindgen_func_elem_233_3(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_233_3(arg0, arg1, addHeapObject(arg2));
 }
 
-function __wasm_bindgen_func_elem_228_3(arg0, arg1, arg2) {
-    wasm.__wasm_bindgen_func_elem_228_3(arg0, arg1, addHeapObject(arg2));
+function __wasm_bindgen_func_elem_515(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_515(arg0, arg1, addHeapObject(arg2));
 }
 
-function __wasm_bindgen_func_elem_510(arg0, arg1, arg2) {
-    wasm.__wasm_bindgen_func_elem_510(arg0, arg1, addHeapObject(arg2));
+function __wasm_bindgen_func_elem_635(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_635(arg0, arg1, addHeapObject(arg2));
 }
 
-function __wasm_bindgen_func_elem_620(arg0, arg1, arg2) {
-    wasm.__wasm_bindgen_func_elem_620(arg0, arg1, addHeapObject(arg2));
+function __wasm_bindgen_func_elem_515_6(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_515_6(arg0, arg1, addHeapObject(arg2));
 }
 
-function __wasm_bindgen_func_elem_302(arg0, arg1, arg2) {
-    wasm.__wasm_bindgen_func_elem_302(arg0, arg1, addHeapObject(arg2));
+function __wasm_bindgen_func_elem_307(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_307(arg0, arg1, addHeapObject(arg2));
 }
 
-function __wasm_bindgen_func_elem_1437(arg0, arg1, arg2) {
+function __wasm_bindgen_func_elem_1460(arg0, arg1, arg2) {
     try {
         const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
-        wasm.__wasm_bindgen_func_elem_1437(retptr, arg0, arg1, addHeapObject(arg2));
+        wasm.__wasm_bindgen_func_elem_1460(retptr, arg0, arg1, addHeapObject(arg2));
         var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
         var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
         if (r1) {
@@ -693,8 +706,8 @@ function __wasm_bindgen_func_elem_1437(arg0, arg1, arg2) {
     }
 }
 
-function __wasm_bindgen_func_elem_229(arg0, arg1, arg2) {
-    wasm.__wasm_bindgen_func_elem_229(arg0, arg1, arg2);
+function __wasm_bindgen_func_elem_234(arg0, arg1, arg2) {
+    wasm.__wasm_bindgen_func_elem_234(arg0, arg1, arg2);
 }
 
 function addHeapObject(obj) {
diff --git a/crates/apps/gioser-web/pkg/gioser_web_bg.wasm b/crates/apps/gioser-web/pkg/gioser_web_bg.wasm
index c8df104..45c7ee6 100644
Binary files a/crates/apps/gioser-web/pkg/gioser_web_bg.wasm and b/crates/apps/gioser-web/pkg/gioser_web_bg.wasm differ
diff --git a/crates/apps/gioser-web/pkg/gioser_web_bg.wasm.d.ts b/crates/apps/gioser-web/pkg/gioser_web_bg.wasm.d.ts
index 370d339..91b6d92 100644
--- a/crates/apps/gioser-web/pkg/gioser_web_bg.wasm.d.ts
+++ b/crates/apps/gioser-web/pkg/gioser_web_bg.wasm.d.ts
@@ -2,14 +2,15 @@
 /* eslint-disable */
 export const memory: WebAssembly.Memory;
 export const boot: () => void;
-export const __wasm_bindgen_func_elem_229: (a: number, b: number, c: number) => void;
-export const __wasm_bindgen_func_elem_1437: (a: number, b: number, c: number, d: number) => void;
-export const __wasm_bindgen_func_elem_228: (a: number, b: number, c: number) => void;
-export const __wasm_bindgen_func_elem_228_3: (a: number, b: number, c: number) => void;
-export const __wasm_bindgen_func_elem_510: (a: number, b: number, c: number) => void;
-export const __wasm_bindgen_func_elem_620: (a: number, b: number, c: number) => void;
-export const __wasm_bindgen_func_elem_302: (a: number, b: number, c: number) => void;
-export const __wasm_bindgen_func_elem_303: (a: number, b: number) => void;
+export const __wasm_bindgen_func_elem_234: (a: number, b: number, c: number) => void;
+export const __wasm_bindgen_func_elem_1460: (a: number, b: number, c: number, d: number) => void;
+export const __wasm_bindgen_func_elem_233: (a: number, b: number, c: number) => void;
+export const __wasm_bindgen_func_elem_233_3: (a: number, b: number, c: number) => void;
+export const __wasm_bindgen_func_elem_515: (a: number, b: number, c: number) => void;
+export const __wasm_bindgen_func_elem_635: (a: number, b: number, c: number) => void;
+export const __wasm_bindgen_func_elem_515_6: (a: number, b: number, c: number) => void;
+export const __wasm_bindgen_func_elem_307: (a: number, b: number, c: number) => void;
+export const __wasm_bindgen_func_elem_308: (a: number, b: number) => void;
 export const __wbindgen_export: (a: number, b: number) => number;
 export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
 export const __wbindgen_export3: (a: number) => void;
diff --git a/crates/apps/gioser-web/src/lib.rs b/crates/apps/gioser-web/src/lib.rs
index ee36472..edc8c3f 100644
--- a/crates/apps/gioser-web/src/lib.rs
+++ b/crates/apps/gioser-web/src/lib.rs
@@ -79,9 +79,16 @@ impl AppState {
         self.sync_active_class();
         self.sync_taskbar();
         self.load_md_if_empty(element, md_url);
-        // Actualizar hash sin disparar evento (evitar loop)
+        // Actualizar URL con history.pushState (sin #)
         if let Some(win) = web_sys::window() {
-            let _ = win.location().set_hash(&format!("/{}", element));
+            if let Ok(hist) = win.history() {
+                let path = format!("/estudio/{}", element);
+                let _ = hist.push_state_with_url(
+                    &wasm_bindgen::JsValue::NULL,
+                    "",
+                    Some(&path),
+                );
+            }
         }
     }
 
@@ -110,9 +117,15 @@ impl AppState {
         self.sync_active_class();
         self.sync_taskbar();
         self.hide_deck(origin_x, origin_y);
-        // Limpiar hash
+        // Restaurar URL
         if let Some(win) = web_sys::window() {
-            let _ = win.location().set_hash("");
+            if let Ok(hist) = win.history() {
+                let _ = hist.push_state_with_url(
+                    &wasm_bindgen::JsValue::NULL,
+                    "",
+                    Some("/"),
+                );
+            }
         }
     }
 
@@ -164,6 +177,7 @@ impl AppState {
         if let Some(body) = self.document.body() {
             let _ = body.class_list().add_1("deck-visible");
         }
+        self.sync_page_controls();
     }
 
     fn hide_deck(&self, x: f64, y: f64) {
@@ -175,6 +189,35 @@ impl AppState {
         if let Some(body) = self.document.body() {
             let _ = body.class_list().remove_1("deck-visible");
         }
+        self.sync_page_controls();
+    }
+
+    fn sync_page_controls(&self) {
+        let exists = self.document.get_element_by_id("global-page-controls");
+        let is_visible = self.state.borrow().active.is_some();
+        if let Some(ctl) = exists {
+            ctl.set_attribute("style", if is_visible {
+                "opacity:1;pointer-events:auto;"
+            } else {
+                "opacity:0;pointer-events:none;"
+            }).ok();
+        } else if is_visible {
+            let Some(body) = self.document.body() else { return };
+            let div: HtmlElement = self.document
+                .create_element("div")
+                .ok()
+                .and_then(|e| e.dyn_into().ok())
+                .unwrap();
+            div.set_id("global-page-controls");
+            div.set_attribute("class", "page-controls").ok();
+            div.set_inner_html(
+                "<button class=\"page-control-btn page-minimize\" data-minimize=\"\" type=\"button\" aria-label=\"Minimizar\">\
+                    <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M5 19 H19\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\"/></svg>\
+                </button>\
+                <button class=\"page-control-btn page-close\" data-close-page=\"\" type=\"button\" aria-label=\"Cerrar\">×</button>"
+            );
+            body.append_child(&div).ok();
+        }
     }
 
     fn deck_el(&self) -> Option<HtmlElement> {
@@ -243,12 +286,6 @@ impl AppState {
         };
         let html = format!(
             "<article class=\"deck-page\" data-element=\"{el}\" id=\"deck-page-{el}\">\
-                <div class=\"page-controls\">\
-                    <button class=\"page-control-btn page-minimize\" data-minimize=\"{el}\" type=\"button\" aria-label=\"Minimizar {title}\">\
-                        <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M5 19 H19\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\"/></svg>\
-                    </button>\
-                    <button class=\"page-control-btn page-close\" data-close-page=\"{el}\" type=\"button\" aria-label=\"Cerrar {title}\">×</button>\
-                </div>\
                 <div class=\"page-ambience\" aria-hidden=\"true\"></div>\
                 <header class=\"page-head\">\
                     <span class=\"page-mark\">{el}</span>\
@@ -448,13 +485,13 @@ pub fn boot() -> Result<(), JsValue> {
     install_deck_delegation(&document, &app)?;
     install_taskbar(&document, &app)?;
     install_keyboard(&document, &app)?;
-    install_hash_listener(&window, &app)?;
+    install_popstate_listener(&window, &app)?;
     install_raf(&window, &document, &canvas, &renderer);
 
-    // Leer hash inicial para abrir página directa
-    if let Ok(hash) = window.location().hash() {
-        let clean = hash.trim_start_matches('#').trim_start_matches('/');
-        if !clean.is_empty() && clean != "" {
+    // Leer ruta inicial para abrir página directa
+    if let Ok(pathname) = window.location().pathname() {
+        let clean = pathname.trim_start_matches('/').trim_start_matches("estudio/");
+        if !clean.is_empty() {
             if let Some(el) = document.query_selector(&format!(".tip[data-md][id='tip-{}']", clean)).ok().flatten() {
                 let rect = el.get_bounding_client_rect();
                 let cx = rect.left() + rect.width() / 2.0;
@@ -469,14 +506,14 @@ pub fn boot() -> Result<(), JsValue> {
     Ok(())
 }
 
-fn install_hash_listener(window: &Window, app: &Rc<AppState>) -> Result<(), JsValue> {
+fn install_popstate_listener(window: &Window, app: &Rc<AppState>) -> Result<(), JsValue> {
     let app2 = app.clone();
     let doc = app.document.clone();
     let win2 = window.clone();
     let cb = Closure::<dyn FnMut(Event)>::new(move |_e: Event| {
-        if let Ok(hash) = win2.location().hash() {
-            let clean = hash.trim_start_matches('#').trim_start_matches('/');
-            if clean.is_empty() {
+        if let Ok(pathname) = win2.location().pathname() {
+            let clean = pathname.trim_start_matches('/').trim_start_matches("estudio/");
+            if clean.is_empty() || clean == "/" {
                 app2.home();
             } else if let Some(el) = doc.query_selector(&format!(".tip[data-md][id='tip-{}']", clean)).ok().flatten() {
                 let rect = el.get_bounding_client_rect();
@@ -488,7 +525,7 @@ fn install_hash_listener(window: &Window, app: &Rc<AppState>) -> Result<(), JsVa
             }
         }
     });
-    window.add_event_listener_with_callback("hashchange", cb.as_ref().unchecked_ref())?;
+    window.add_event_listener_with_callback("popstate", cb.as_ref().unchecked_ref())?;
     cb.forget();
     Ok(())
 }
@@ -615,22 +652,35 @@ fn install_deck_delegation(document: &Document, app: &Rc<AppState>) -> Result<()
         if let Ok(Some(btn)) = target_el.closest("[data-minimize]") {
             e.stop_propagation();
             let element = btn.get_attribute("data-minimize").unwrap_or_default();
-            // Origin = la cajita correspondiente en la taskbar (efecto
-            // visual: la página se "encoge" hacia su entrada del taskbar).
-            let origin = app2
-                .taskbar_item_center(&element)
-                .unwrap_or_else(|| center_of_element(&btn));
-            app2.minimize(origin.0, origin.1);
+            // Si el data-minimize está vacío, usar el elemento activo
+            let el = if element.is_empty() {
+                app2.state.borrow().active.clone().unwrap_or_default()
+            } else {
+                element
+            };
+            if !el.is_empty() {
+                let origin = app2
+                    .taskbar_item_center(&el)
+                    .unwrap_or_else(|| center_of_element(&btn));
+                app2.minimize(origin.0, origin.1);
+            }
             return;
         }
         // Close
         if let Ok(Some(btn)) = target_el.closest("[data-close-page]") {
             e.stop_propagation();
             let element = btn.get_attribute("data-close-page").unwrap_or_default();
-            let origin = app2
-                .taskbar_item_center(&element)
-                .unwrap_or_else(|| center_of_element(&btn));
-            app2.close(&element, origin.0, origin.1);
+            let el = if element.is_empty() {
+                app2.state.borrow().active.clone().unwrap_or_default()
+            } else {
+                element
+            };
+            if !el.is_empty() {
+                let origin = app2
+                    .taskbar_item_center(&el)
+                    .unwrap_or_else(|| center_of_element(&btn));
+                app2.close(&el, origin.0, origin.1);
+            }
         }
     });
     deck_el.add_event_listener_with_callback("click", cb.as_ref().unchecked_ref())?;
diff --git a/crates/apps/gioser-web/styles.css b/crates/apps/gioser-web/styles.css
index 43bfe56..bee8dc0 100644
--- a/crates/apps/gioser-web/styles.css
+++ b/crates/apps/gioser-web/styles.css
@@ -254,14 +254,21 @@ body.deck-active-tierra .deck { --deck-glow: rgba(212, 152, 115, 0.24); }
   to   { opacity: 0.80; }
 }
 
-/* Head + controls */
+/* Head + controls — fijos en el deck, no dentro de la página */
 .page-controls {
-  position: absolute;
-  top: 1.8vh;
-  right: 1.8vw;
-  z-index: 5;
+  position: fixed;
+  top: 1.2rem;
+  right: 1.2rem;
+  z-index: 100;
   display: flex;
   gap: 0.5rem;
+  opacity: 0;
+  pointer-events: none;
+  transition: opacity 0.3s ease;
+}
+.deck--visible .page-controls {
+  opacity: 1;
+  pointer-events: auto;
 }
 .page-control-btn {
   width: 40px;
diff --git a/crates/modules/gioser/gioser-graph-web/Cargo.toml b/crates/modules/gioser/gioser-graph-web/Cargo.toml
index 7eac66b..e54eb29 100644
--- a/crates/modules/gioser/gioser-graph-web/Cargo.toml
+++ b/crates/modules/gioser/gioser-graph-web/Cargo.toml
@@ -35,5 +35,6 @@ features = [
     "Event",
     "EventTarget",
     "MouseEvent",
+    "PointerEvent",
     "console",
 ]
diff --git a/crates/modules/gioser/gioser-graph-web/src/lib.rs b/crates/modules/gioser/gioser-graph-web/src/lib.rs
index 3bcb260..36c4f66 100644
--- a/crates/modules/gioser/gioser-graph-web/src/lib.rs
+++ b/crates/modules/gioser/gioser-graph-web/src/lib.rs
@@ -15,7 +15,7 @@ use wasm_bindgen::prelude::*;
 use wasm_bindgen::JsCast;
 use wasm_bindgen_futures::JsFuture;
 use web_sys::{
-    Document, HtmlElement, MouseEvent, Response, SvgLineElement, SvgRectElement,
+    Document, HtmlElement, MouseEvent, PointerEvent, Response, SvgLineElement, SvgRectElement,
     SvgsvgElement, SvgTextElement, SvgCircleElement,
 };
 
@@ -204,13 +204,6 @@ impl GraphWidget {
             .gb-node { transition: filter 250ms ease, opacity 200ms ease; }\
             .gb-node:hover {\
               filter: drop-shadow(0 0 14px rgba(255,255,255,0.2));\
-              animation: node-bounce 0.4s ease-out;\
-            }\
-            @keyframes node-bounce {\
-              0% { transform: scale(1); }\
-              40% { transform: scale(1.08); }\
-              70% { transform: scale(0.96); }\
-              100% { transform: scale(1); }\
             }\
             .gb-edge-group { pointer-events: none; }\
             .gb-line { transition: opacity 400ms ease; }",
@@ -395,33 +388,37 @@ impl GraphWidget {
             g.append_child(&text).ok();
             g.append_child(&sub).ok();
 
-            // Hover
-            let rect_clone = rect.clone();
-            let color_c = color.clone();
-            let glow_clone = glow.clone();
+            // Hover + drag
+            let rect_h = rect.clone();
+            let col_h = color.clone();
+            let glow_h = glow.clone();
+            let g_hover = g.clone();
+            let g_clone_for_drag = g.clone();
+
             let enter = Closure::<dyn FnMut(MouseEvent)>::new(move |_| {
-                rect_clone.set_attribute("fill-opacity", "0.55").ok();
-                rect_clone.set_attribute("stroke-opacity", "1").ok();
-                rect_clone.style()
-                    .set_property("filter", &format!("drop-shadow(0 0 12px {})", color_c))
+                rect_h.set_attribute("fill-opacity", "0.55").ok();
+                rect_h.set_attribute("stroke-opacity", "1").ok();
+                rect_h.style()
+                    .set_property("filter", &format!("drop-shadow(0 0 12px {})", col_h))
                     .ok();
-                glow_clone.set_attribute("fill-opacity", "0.20").ok();
+                glow_h.set_attribute("fill-opacity", "0.20").ok();
             });
-            g.add_event_listener_with_callback("mouseenter", enter.as_ref().unchecked_ref()).ok();
+            g_hover.add_event_listener_with_callback("mouseenter", enter.as_ref().unchecked_ref()).ok();
             enter.forget();
 
-            let rect_clone2 = rect.clone();
-            let glow_clone2 = glow.clone();
+            let rect_l = rect.clone();
+            let glow_l = glow.clone();
             let leave = Closure::<dyn FnMut(MouseEvent)>::new(move |_| {
-                rect_clone2.set_attribute("fill-opacity", "0.28").ok();
-                rect_clone2.set_attribute("stroke-opacity", "0.7").ok();
-                rect_clone2.style().set_property("filter", "none").ok();
-                glow_clone2.set_attribute("fill-opacity", "0.05").ok();
+                rect_l.set_attribute("fill-opacity", "0.28").ok();
+                rect_l.set_attribute("stroke-opacity", "0.7").ok();
+                rect_l.style().set_property("filter", "none").ok();
+                glow_l.set_attribute("fill-opacity", "0.05").ok();
             });
             g.add_event_listener_with_callback("mouseleave", leave.as_ref().unchecked_ref()).ok();
             leave.forget();
 
-            let nav_target = node.camino.clone(); // pasamos el camino (logos, nomos, etc)
+            // Click → navegar
+            let nav_target = node.camino.clone();
             let on_nav2 = on_nav.clone();
             let click = Closure::<dyn FnMut(MouseEvent)>::new(move |_| {
                 let mut cb = on_nav2.borrow_mut();
@@ -430,6 +427,50 @@ impl GraphWidget {
             g.add_event_listener_with_callback("click", click.as_ref().unchecked_ref()).ok();
             click.forget();
 
+            // Drag: pointerdown → move → up (reacomodar al soltar)
+            let g_drag = g.clone();
+            let g_move = g.clone();
+            let g_up = g.clone();
+            let drag_start = Rc::new(RefCell::new(None::<(f64, f64, f64, f64)>));
+            let drag_start2 = drag_start.clone();
+            let drag_start3 = drag_start.clone();
+
+            let pdown = Closure::<dyn FnMut(PointerEvent)>::new(move |e: PointerEvent| {
+                e.prevent_default();
+                let g_rect = g_drag.get_bounding_client_rect();
+                *drag_start.borrow_mut() = Some((
+                    e.client_x() as f64,
+                    e.client_y() as f64,
+                    g_rect.left() + g_rect.width() / 2.0,
+                    g_rect.top() + g_rect.height() / 2.0,
+                ));
+                g_drag.set_pointer_capture(e.pointer_id()).ok();
+            });
+            g.add_event_listener_with_callback("pointerdown", pdown.as_ref().unchecked_ref()).ok();
+            pdown.forget();
+
+            let pmove = Closure::<dyn FnMut(PointerEvent)>::new(move |e: PointerEvent| {
+                if let Some((start_cx, start_cy, _, _)) = *drag_start2.borrow() {
+                    let dx = e.client_x() as f64 - start_cx;
+                    let dy = e.client_y() as f64 - start_cy;
+                    g_move.set_attribute(
+                        "transform",
+                        &format!("translate({:.1},{:.1})", dx, dy),
+                    ).ok();
+                }
+            });
+            g.add_event_listener_with_callback("pointermove", pmove.as_ref().unchecked_ref()).ok();
+            pmove.forget();
+
+            let pup = Closure::<dyn FnMut(PointerEvent)>::new(move |_e: PointerEvent| {
+                *drag_start3.borrow_mut() = None;
+                g_up.set_attribute("transform", "translate(0,0)").ok();
+            });
+            g.add_event_listener_with_callback("pointerup", pup.as_ref().unchecked_ref()).ok();
+            pup.forget();
+
+            g.style().set_property("transition", "transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)").ok();
+
             nodes_group.append_child(&g).ok();
         }