humanmap-vas 1.0.23 → 1.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@
7
7
  .hm-center { text-align:center; font-weight:600; color:#1f2937; }
8
8
  .hm-toolbar select, .hm-toolbar button { appearance:none; border:1px solid #d1d5db; border-radius:10px; padding:6px 10px; background:#fff; cursor:pointer; font-weight:500; }
9
9
  .hm-canvas-wrap { position:relative; width:100%; height:600px; margin:auto; aspect-ratio: 2/3; background:#fff; }
10
- svg.hm-svg { position:absolute; inset:0; width:100%; height:100%; }
10
+ svg.hm-svg { position:absolute; inset:0; width:100%; height:100%; margin: auto; }
11
11
  .zone { fill: rgba(31,41,55,0); transition: fill 120ms ease; cursor: pointer; }
12
12
  .zone:hover { fill: rgba(31,41,55,0.22); }
13
13
  .zone.readonly { cursor: default; }
@@ -18,6 +18,19 @@
18
18
  .hm-print-btn { position: absolute; top: 10px; right: 10px; background: rgba(17,24,39,0.85); color: #f9fafb; border: none; border-radius: 8px; cursor: pointer; font-size: 18px; padding: 6px 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); transition: opacity 0.25s ease, background 0.2s ease; opacity: 0; pointer-events: none; }
19
19
  .hm-canvas-wrap:hover .hm-print-btn { opacity: 1; pointer-events: auto; }
20
20
  .hm-print-btn:hover { background: rgba(37,99,235,0.9); }
21
+ .hm-zoom-float { position: absolute; bottom: 10px; right: 10px; background: rgba(31,41,55,0.85); color: #fff; border: none; border-radius: 50%; width: 42px; height: 42px; font-size: 20px; line-height: 1; cursor: pointer; box-shadow: 0 3px 8px rgba(0,0,0,0.3); transition: transform 0.2s ease, background 0.2s ease; z-index: 20; }
22
+ .hm-zoom-float:hover { transform: scale(1.08); background: rgba(31,41,55,1); }
23
+ .hm-zoom-modal { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.85); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; }
24
+ .hm-zoom-modal.active { opacity: 1; pointer-events: auto; }
25
+ .hm-zoom-inner { position: relative; width: 95vw; height: 90vh; background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; }
26
+ .hm-zoom-content { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #fff; overflow: auto; }
27
+ .hm-zoom-close { position: absolute; top: 12px; right: 12px; border: none; background: rgba(0, 0, 0, 0.75); color: #fff; border-radius: 50%; width: 42px; height: 42px; font-size: 24px; cursor: pointer; z-index: 10000; line-height: 1; }
28
+ .hm-zoom-close:hover { background: rgba(0, 0, 0, 0.9); }
29
+ .hm-zoom-modal svg { width: auto; height: auto; max-width: 100%; max-height: 100%; display: block; transition: transform 0.25s ease; }
30
+ .hm-zoom-inner { transform: scale(0.95); opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; }
31
+ .hm-zoom-modal.active .hm-zoom-inner { transform: scale(1); opacity: 1; }
32
+ .hm-zoom-hint { position: absolute; bottom: 10px; right: 20px; color: rgba(0,0,0,0.4); font-size: 14px; font-family: system-ui, sans-serif; background: rgba(255,255,255,0.7); padding: 4px 10px; border-radius: 6px; pointer-events: none; user-select: none; transition: opacity 1s ease 2s; opacity: 1; }
33
+
21
34
  `;
22
35
 
23
36
  // ───────────────────────────────────────────────────────────────────────────
@@ -175,6 +188,13 @@
175
188
  this.dispatchEvent(new CustomEvent('human-map-vas:readonly-change', {
176
189
  detail: { readOnly: this._readOnly }
177
190
  }));
191
+
192
+ window.addEventListener('resize', () => {
193
+ if (this._view === 'all') {
194
+ clearTimeout(this._resizeTimer);
195
+ this._resizeTimer = setTimeout(() => this._renderAllViews(), 150);
196
+ }
197
+ });
178
198
  }
179
199
 
180
200
  static get observedAttributes() { return ['view', 'img-root', 'read-only']; }
@@ -185,6 +205,11 @@
185
205
  if (name === 'view') {
186
206
  this._view = newValue;
187
207
  if (this._root) this._renderCanvas();
208
+
209
+ this.dispatchEvent(new CustomEvent('human-map-vas:view-changed', {
210
+ detail: { view: newValue }
211
+ }));
212
+
188
213
  return;
189
214
  }
190
215
 
@@ -312,6 +337,7 @@
312
337
  <g id="zones"></g>
313
338
  </svg>
314
339
  <button id="printBtn" title="Imprimir vista" class="hm-print-btn">🖨️</button>
340
+ <button id="zoom-float" class="hm-zoom-float" title="Ampliar vista">⤢</button>
315
341
  </div>`;
316
342
  this.shadowRoot.append(style,this._root);
317
343
 
@@ -338,6 +364,8 @@
338
364
  this._printCanvasOnly(); // ⬅️ imprime solo el área actual
339
365
  }, 120);
340
366
  });
367
+ this._els.zoomFloat = this.shadowRoot.getElementById('zoom-float');
368
+ this._els.zoomFloat.addEventListener('click', () => this._openPreviewModal());
341
369
 
342
370
  }
343
371
 
@@ -368,6 +396,10 @@
368
396
  }
369
397
  }
370
398
 
399
+ if (this._els.zoomFloat) {
400
+ this._els.zoomFloat.style.display = this._readOnly ? 'block' : 'none';
401
+ }
402
+
371
403
  // Limpia cualquier render anterior (modo all o vista única)
372
404
  if (this._els && this._els.svg) {
373
405
  this._els.svg.innerHTML = `
@@ -411,9 +443,14 @@
411
443
  const svg = this._els.svg;
412
444
  svg.innerHTML = ''; // limpiar
413
445
 
414
- // Creamos un grupo por cada vista
415
- const cols = 2, gap = 40;
416
- const cellW = 480, cellH = 720;
446
+ // Permitir configurar columnas dinámicas (por atributo) o calcular automáticamente
447
+ const defaultCols = 4;
448
+ const attrCols = parseInt(this.getAttribute('columns'), 10);
449
+ let cols = !isNaN(attrCols) && attrCols > 0 ? attrCols : defaultCols;
450
+
451
+ const gap = 80;
452
+ const cellW = 560, cellH = 720;
453
+
417
454
  const gridW = cols * (cellW + gap);
418
455
  const gridH = Math.ceil(VIEWS.length / cols) * (cellH + gap);
419
456
 
@@ -429,7 +466,9 @@
429
466
  const gy = row * (cellH + gap);
430
467
 
431
468
  const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
432
- g.setAttribute('transform', `translate(${gx}, ${gy}) scale(0.45)`);
469
+ // Escala dinámica: reduce más si hay muchas columnas
470
+ const scale = Math.max(0.3, 0.9 - cols * 0.1);
471
+ g.setAttribute('transform', `translate(${gx}, ${gy}) scale(${scale})`);
433
472
 
434
473
  // Fondo (imagen)
435
474
  const img = document.createElementNS('http://www.w3.org/2000/svg', 'image');
@@ -511,6 +550,11 @@
511
550
 
512
551
  gRoot.appendChild(g);
513
552
  });
553
+
554
+ if (this._els.zoomFloat) {
555
+ this._els.zoomFloat.style.display = this._readOnly ? 'block' : 'none';
556
+ }
557
+
514
558
  }
515
559
 
516
560
  _renderZones(){
@@ -665,6 +709,156 @@
665
709
  });
666
710
  }
667
711
 
712
+ _openPreviewModal() {
713
+ if (!this._els || !this._els.svg) return;
714
+
715
+ // Clonar el SVG actual
716
+ const clone = this._els.svg.cloneNode(true);
717
+
718
+ // Asegurar que las imágenes se vean correctamente (usar rutas absolutas)
719
+ clone.querySelectorAll('image').forEach(img => {
720
+ const href = img.getAttribute('href') || img.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
721
+ if (href) {
722
+ const a = document.createElement('a');
723
+ a.href = href;
724
+ const abs = a.href;
725
+ img.setAttribute('href', abs);
726
+ img.setAttributeNS('http://www.w3.org/1999/xlink', 'href', abs);
727
+ }
728
+ });
729
+
730
+ // Ajustar estilos dentro del SVG
731
+ const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
732
+ style.textContent = `
733
+ .zone { fill: rgba(31,41,55,0); cursor: default; transition: fill 120ms ease; }
734
+ .zone.selected { fill: rgba(31,41,55,0.36); }
735
+ .label { fill: #0a0a0a; font-size: 42px; font-weight: 800;
736
+ text-anchor: middle; dominant-baseline: middle;
737
+ pointer-events: none; user-select: none; }
738
+ .hm-all-label {
739
+ font-family: system-ui, sans-serif;
740
+ font-size: 48px;
741
+ font-weight: 800;
742
+ fill: #111827;
743
+ text-anchor: middle;
744
+ dominant-baseline: middle;
745
+ }
746
+ `;
747
+ clone.insertBefore(style, clone.firstChild);
748
+
749
+ // Crear el modal
750
+ const modal = document.createElement('div');
751
+ modal.className = 'hm-zoom-modal';
752
+ modal.innerHTML = `
753
+ <div class="hm-zoom-inner">
754
+ <button class="hm-zoom-close" title="Cerrar">×</button>
755
+ <div class="hm-zoom-content"></div>
756
+ <div class="hm-zoom-hint">🖱️ Usa la rueda para hacer zoom y arrastra para mover. Presiona <strong>Esc</strong> para cerrar</div>
757
+ </div>
758
+ `;
759
+
760
+ // Insertar el SVG clonado dentro del modal
761
+ modal.querySelector('.hm-zoom-content').appendChild(clone);
762
+ this.shadowRoot.appendChild(modal);
763
+
764
+ // Forzar render y animación
765
+ requestAnimationFrame(() => modal.classList.add('active'));
766
+ // 🔒 Bloquear scroll del fondo
767
+ document.body.style.overflow = 'hidden';
768
+
769
+ // Cerrar modal
770
+ const close = () => {
771
+ modal.classList.remove('active');
772
+
773
+ // 🔓 Restaurar scroll del fondo
774
+ document.body.style.overflow = '';
775
+
776
+ setTimeout(() => modal.remove(), 300);
777
+
778
+ // Quitar listener del teclado al cerrar
779
+ document.removeEventListener('keydown', onKey);
780
+ };
781
+
782
+ modal.querySelector('.hm-zoom-close').addEventListener('click', close);
783
+ modal.addEventListener('click', e => {
784
+ if (e.target === modal) close();
785
+ });
786
+
787
+ // 🔑 Cerrar con tecla Escape
788
+ const onKey = e => {
789
+ if (e.key === 'Escape') close();
790
+ };
791
+ document.addEventListener('keydown', onKey);
792
+
793
+ // ───────────────────────────────
794
+ // 🔍 Zoom + Pan interactivo
795
+ // ───────────────────────────────
796
+ const content = modal.querySelector('.hm-zoom-content');
797
+ let scale = 1;
798
+ let translateX = 0, translateY = 0;
799
+ let isPanning = false;
800
+ let startX = 0, startY = 0;
801
+
802
+ content.addEventListener('wheel', e => {
803
+ e.preventDefault();
804
+
805
+ const rect = clone.getBoundingClientRect();
806
+ const offsetX = e.clientX - rect.left;
807
+ const offsetY = e.clientY - rect.top;
808
+
809
+ const delta = e.deltaY < 0 ? 0.1 : -0.1;
810
+ const newScale = Math.min(Math.max(scale + delta, 0.5), 3);
811
+
812
+ // Mantener el punto bajo el puntero "anclado"
813
+ const dx = offsetX - (offsetX / scale) * newScale;
814
+ const dy = offsetY - (offsetY / scale) * newScale;
815
+ translateX += dx;
816
+ translateY += dy;
817
+
818
+ clone.style.transform = `translate(${translateX}px, ${translateY}px) scale(${newScale})`;
819
+ clone.style.transformOrigin = '0 0';
820
+ scale = newScale;
821
+ });
822
+
823
+ // ───────────────────────────────
824
+ // 🖱️ Arrastrar (Pan)
825
+ // ───────────────────────────────
826
+ content.addEventListener('mousedown', e => {
827
+ e.preventDefault();
828
+ isPanning = true;
829
+ startX = e.clientX - translateX;
830
+ startY = e.clientY - translateY;
831
+ content.style.cursor = 'grabbing';
832
+ });
833
+
834
+ content.addEventListener('mousemove', e => {
835
+ if (!isPanning) return;
836
+ translateX = e.clientX - startX;
837
+ translateY = e.clientY - startY;
838
+ clone.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
839
+ });
840
+
841
+ content.addEventListener('mouseup', () => {
842
+ isPanning = false;
843
+ content.style.cursor = 'default';
844
+ });
845
+
846
+ content.addEventListener('mouseleave', () => {
847
+ isPanning = false;
848
+ content.style.cursor = 'default';
849
+ });
850
+
851
+ // ───────────────────────────────
852
+ // 🔄 Doble clic para resetear vista
853
+ // ───────────────────────────────
854
+ content.addEventListener('dblclick', () => {
855
+ scale = 1;
856
+ translateX = 0;
857
+ translateY = 0;
858
+ clone.style.transform = '';
859
+ });
860
+
861
+ }
668
862
 
669
863
  _emit(){this.dispatchEvent(new CustomEvent('human-map-vas:select',{detail:{selected:this.getSelected()}}));}
670
864
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanmap-vas",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "**HumanMap VAS** es una librería web que permite graficar el cuerpo humano con vistas anatómicas interactivas para identificar zonas según el sistema VAS. Desarrollada como *Web Component standalone*, puede integrarse fácilmente en proyectos **HTML**, **Django**, o **Vue.js**.",
5
5
  "main": "humanmap-vas-standalone.js",
6
6
  "files": [