// Organización tipo app (28/sep): tarjetas ilustradas, vista previa al mantener // presionado y barra de abajo en el celular. Es el mismo diseño que la app de // Android (paquete «paquete_web_iconos_organizacion» del usuario). // // Los 141 dibujos viven en assets/iconos/ y su nombre sale directo de los datos // (ver orgMapa): así funcionan igual aunque se agregue un diagnóstico, siempre // que su clase ya tenga dibujo. Se verificó contra el manifiesto del paquete: los // 27 dominios y las 104 clases coinciden, nombre por nombre. const { useState: useOrgS, useEffect: useOrgE, useRef: useOrgR } = React; // Sube este número si se redibuja algún ícono: el .htaccess los guarda 7 días. const ORG_ICONOS_V = '20260928'; const orgIconoUrl = (slot) => 'assets/iconos/' + slot + '.webp?v=' + ORG_ICONOS_V; const ORG_PREFIJO = { dx: 'dx', ob: 'no', iv: 'ni' }; const ORG_UNIDAD = { dx: ['diagnóstico', 'diagnósticos'], ob: ['objetivo', 'objetivos'], iv: ['intervención', 'intervenciones'] }; const orgContar = (n, kind) => n + ' ' + ORG_UNIDAD[kind][n === 1 ? 0 : 1]; // Intervenciones se organiza en «campos»; los otros dos libros, en «dominios». const orgNivel1 = (kind, plural) => (kind === 'iv' ? (plural ? 'campos' : 'Campo') : (plural ? 'dominios' : 'Dominio')); // La clase de un elemento: diagnósticos por clase_num, objetivos por // clase_codigo, e intervenciones (sin código en los datos) por su nombre. function orgClaseDe(kind, it) { if (kind === 'dx') return String(it.clase_num); if (kind === 'ob') return String(it.clase_codigo); return it.clase || ''; } // Dominios y clases de un libro, sacados de los datos (no de una lista fija). // Los dibujos de intervenciones se numeran por orden de aparición de la clase // dentro de su campo: es la misma regla con la que se nombraron (28/sep). const orgCacheMapa = {}; function orgMapa(kind) { if (orgCacheMapa[kind]) return orgCacheMapa[kind]; const src = kind === 'dx' ? DATA.dxs : kind === 'ob' ? DATA.objs : DATA.ints; const pre = ORG_PREFIJO[kind]; const nombreDom = new Map((src.dominios || []).map(d => [Number(d.num), d.name])); const doms = new Map(); for (const it of src.items) { const dn = Number(it.dominio_num); if (!doms.has(dn)) { const nombre = String(nombreDom.get(dn) || it.dominio || '').replace(/^(Dominio|Campo)\s*\d+:?\s*/i, ''); doms.set(dn, { num: dn, nombre, slot: 'dom-' + pre + '-' + dn, clases: new Map(), total: 0 }); } const d = doms.get(dn); d.total++; const k = orgClaseDe(kind, it); if (!d.clases.has(k)) { const codigo = kind === 'iv' ? String(d.clases.size + 1) : k; d.clases.set(k, { k, codigo, etiqueta: kind === 'iv' ? null : 'Clase ' + k, nombre: String(it.clase || '').replace(/^Clase\s*[\w\d]+:?\s*/i, ''), slot: 'cls-' + pre + '-' + dn + '-' + codigo, items: [], }); } d.clases.get(k).items.push(it); } const porCodigo = (a, b) => String(a.codigo).localeCompare(String(b.codigo), 'es', { numeric: true }); const ordenClase = kind === 'dx' ? (a, b) => Number(a.k) - Number(b.k) : kind === 'ob' // A…Z y luego AA, BB…, como el libro. ? (a, b) => (a.k.length - b.k.length) || a.k.localeCompare(b.k) : () => 0; // intervenciones: orden de aparición const lista = [...doms.values()].sort((a, b) => a.num - b.num).map(d => ({ ...d, clases: [...d.clases.values()].sort(ordenClase).map(c => ({ ...c, items: c.items.slice().sort(porCodigo) })), })); orgCacheMapa[kind] = lista; return lista; } // ─── Vista previa: mantener presionado (táctil) o dejar el cursor encima ──── // En el celular se abre a los 380 ms y se cierra al soltar, SIN entrar. Con // mouse se abre cuando el cursor, después de moverse, se queda 400 ms quieto // sobre la tarjeta; se cierra al salir o al desplazar la página; el clic entra. // Última posición del mouse. Al entrar a un nivel o al desplazar la página, el // navegador avisa que el cursor pasó a la tarjeta que quedó debajo aunque nadie // lo haya movido: sin esta comprobación la vista previa se abría sola (28/sep). const orgMouse = { x: -1, y: -1 }; function useOrgPrevia() { const [vista, setVista] = useOrgS(null); const [tactil, setTactil] = useOrgS(true); const reloj = useOrgR(null); const disparada = useOrgR(false); const abierta = useOrgR(null); // tarjeta cuya vista previa está abierta (mouse) useOrgE(() => () => clearTimeout(reloj.current), []); // Con el mouse, al desplazar la página la tarjeta ya no está bajo el cursor. useOrgE(() => { if (!vista || tactil) return; const cerrar = () => { abierta.current = null; setVista(null); }; window.addEventListener('scroll', cerrar, { passive: true }); return () => window.removeEventListener('scroll', cerrar); }, [vista, tactil]); const parar = () => clearTimeout(reloj.current); const gestos = (armar, entrar) => ({ onPointerDown: (e) => { if (e.pointerType === 'mouse') return; parar(); disparada.current = false; reloj.current = setTimeout(() => { disparada.current = true; setTactil(true); try { if (navigator.vibrate) navigator.vibrate(10); } catch (err) {} setVista(armar()); }, 380); }, onPointerUp: (e) => { if (e.pointerType === 'mouse') return; parar(); if (disparada.current) setVista(null); }, onPointerCancel: () => { parar(); setVista(null); }, onPointerMove: (e) => { if (e.pointerType !== 'mouse') return; const movio = e.clientX !== orgMouse.x || e.clientY !== orgMouse.y; orgMouse.x = e.clientX; orgMouse.y = e.clientY; if (!movio || abierta.current === e.currentTarget) return; // Cada movimiento reinicia la espera: se abre al quedarse quieto. parar(); const el = e.currentTarget; reloj.current = setTimeout(() => { abierta.current = el; setTactil(false); setVista({ ...armar(), rect: el.getBoundingClientRect() }); }, 400); }, onPointerLeave: () => { parar(); abierta.current = null; setVista(null); }, onContextMenu: (e) => e.preventDefault(), onClick: () => { // Soltar después de asomarse no cuenta como toque: solo cierra. if (disparada.current) { disparada.current = false; return; } parar(); abierta.current = null; setVista(null); entrar(); }, }); return { vista, tactil, gestos }; } // Con el dedo: tarjeta centrada sobre la pantalla oscurecida (como la app). // Con el mouse: tarjetita junto a la tarjeta señalada, sin oscurecer nada, para // que detenerse a leer la rejilla no tape la pantalla entera. function orgPosicionCursor(r) { const W = 360, M = 12, vw = window.innerWidth, vh = window.innerHeight; let left = r.right + M; if (left + W > vw - M) left = r.left - M - W; if (left < M) left = Math.max(M, Math.min(vw - W - M, r.left + r.width / 2 - W / 2)); const top = Math.max(M, r.top); return (vh - top >= 300) ? { left, top, width: W, maxHeight: vh - top - M } : { left, bottom: Math.max(M, vh - r.bottom), width: W, maxHeight: Math.max(160, r.bottom - M) }; } function OrgPrevia({ vista, kind, tactil }) { if (!vista) return null; const tono = kind === 'dx' ? 'nanda' : kind === 'ob' ? 'noc' : 'nic'; const junto = !tactil && vista.rect; // Portal al
: .fade-in deja un transform en la sección y un «fixed» // dentro de ella dejaría de cubrir la pantalla. return ReactDOM.createPortal( , document.body ); } // ─── Tarjeta ilustrada (dominio, clase o herramienta) ─────────────────────── function OrgTile({ slot, nombre, sub, tono, on, gestos }) { return ( ); } // ─── Mapa de taxonomías: rejillas de dominios y de clases ─────────────────── // Lo usa DxSection (AppView.jsx). Aquí solo van las rejillas y la cabecera de // nivel; la lista de elementos y la ficha siguen siendo las de siempre. function OrgRejillaDominios({ kind, mapa, onEntrar }) { const { vista, tactil, gestos } = useOrgPrevia(); const tono = kind === 'dx' ? 'nanda' : kind === 'ob' ? 'noc' : 'nic'; const libro = kind === 'dx' ? 'Diagnósticos' : kind === 'ob' ? 'Objetivos' : 'Intervenciones'; return (