valtech-components 4.0.167 → 4.0.169

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.
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
56
56
  * Current version of valtech-components.
57
57
  * This is automatically updated during the publish process.
58
58
  */
59
- const VERSION = '4.0.167';
59
+ const VERSION = '4.0.169';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -35283,6 +35283,10 @@ class HtmlViewerModalComponent {
35283
35283
  constructor() {
35284
35284
  this.html = '';
35285
35285
  this.title = 'documento';
35286
+ this.sanitizer = inject(DomSanitizer);
35287
+ }
35288
+ get safeHtml() {
35289
+ return this.sanitizer.bypassSecurityTrustHtml(this.html);
35286
35290
  }
35287
35291
  dismiss() {
35288
35292
  this._modalRef?.dismiss();
@@ -35323,7 +35327,7 @@ class HtmlViewerModalComponent {
35323
35327
  </ion-header>
35324
35328
 
35325
35329
  <ion-content>
35326
- <iframe [srcdoc]="html" style="width: 100%; height: 100%; border: none;" title="Visor de documento"></iframe>
35330
+ <iframe [srcdoc]="safeHtml" style="width: 100%; height: 100%; border: none;" title="Visor de documento"></iframe>
35327
35331
  </ion-content>
35328
35332
  `, isInline: true, styles: [":host{display:flex;flex-direction:column}ion-content{--padding-bottom: 0;--padding-top: 0}iframe{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
35329
35333
  }
@@ -35349,7 +35353,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
35349
35353
  </ion-header>
35350
35354
 
35351
35355
  <ion-content>
35352
- <iframe [srcdoc]="html" style="width: 100%; height: 100%; border: none;" title="Visor de documento"></iframe>
35356
+ <iframe [srcdoc]="safeHtml" style="width: 100%; height: 100%; border: none;" title="Visor de documento"></iframe>
35353
35357
  </ion-content>
35354
35358
  `, styles: [":host{display:flex;flex-direction:column}ion-content{--padding-bottom: 0;--padding-top: 0}iframe{display:block}\n"] }]
35355
35359
  }], propDecorators: { html: [{
@@ -61516,14 +61520,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
61516
61520
  // Use LocaleService for language management instead
61517
61521
 
61518
61522
  const CANVAS_WIDTH = 560;
61519
- const HEADER_H = 160;
61520
- const QR_ZONE_H = 380;
61521
- const FOOTER_H = 240;
61522
- const CANVAS_HEIGHT = HEADER_H + QR_ZONE_H + FOOTER_H;
61523
- const QR_SIZE = 280;
61523
+ const HEADER_H = 210;
61524
+ const QR_ZONE_H = 340;
61525
+ const PERF_H = 28;
61526
+ const FOOTER_H = 220;
61527
+ const CANVAS_HEIGHT = HEADER_H + QR_ZONE_H + PERF_H + FOOTER_H;
61528
+ const QR_SIZE = 260;
61524
61529
  const DEFAULT_BRAND_COLOR = '#9b1fde';
61525
61530
  const DEFAULT_BRAND_COLOR_TO = '#c93075';
61526
- const FONT_STACK = 'Khula, Arial, sans-serif';
61531
+ const FONT_STACK = '"Plus Jakarta Sans", Khula, Arial, sans-serif';
61527
61532
  class TicketCardImageService {
61528
61533
  async generate(config) {
61529
61534
  const width = config.width ?? CANVAS_WIDTH;
@@ -61541,6 +61546,7 @@ class TicketCardImageService {
61541
61546
  }
61542
61547
  await this.drawHeader(ctx, config, width);
61543
61548
  await this.drawQrZone(ctx, config, width);
61549
+ this.drawPerforated(ctx, config, width);
61544
61550
  this.drawFooter(ctx, config, width);
61545
61551
  if (canvas instanceof OffscreenCanvas) {
61546
61552
  return canvas.convertToBlob({ type: 'image/png' });
@@ -61594,31 +61600,41 @@ class TicketCardImageService {
61594
61600
  grad.addColorStop(1, colorTo);
61595
61601
  ctx.fillStyle = grad;
61596
61602
  ctx.fillRect(0, 0, width, HEADER_H);
61597
- const logoH = 44;
61598
- const logoPad = 28;
61599
- let textY = HEADER_H / 2;
61603
+ const logoH = 48;
61604
+ const logoPadTop = 28;
61605
+ let cursorY = logoPadTop;
61600
61606
  if (config.logoUrl) {
61601
61607
  try {
61602
61608
  const logoImg = await this.loadImage(config.logoUrl);
61603
61609
  const ratio = logoImg.width / logoImg.height;
61604
61610
  const logoW = logoH * ratio;
61605
- const logoX = logoPad;
61606
- const logoY = (HEADER_H - logoH) / 2;
61607
- ctx.drawImage(logoImg, logoX, logoY, logoW, logoH);
61608
- if (config.brandName) {
61609
- textY = logoY + logoH + 14;
61610
- }
61611
+ const logoX = (width - logoW) / 2;
61612
+ ctx.drawImage(logoImg, logoX, cursorY, logoW, logoH);
61613
+ cursorY += logoH + 14;
61611
61614
  }
61612
61615
  catch {
61613
- // logo failed — render brand name centered as fallback
61616
+ // logo failed — continue without it
61614
61617
  }
61615
61618
  }
61616
61619
  if (config.brandName) {
61617
61620
  ctx.fillStyle = '#ffffff';
61618
- ctx.font = `bold 1.75rem ${FONT_STACK}`;
61621
+ ctx.font = `bold 1.625rem ${FONT_STACK}`;
61619
61622
  ctx.textAlign = 'center';
61620
- ctx.textBaseline = 'middle';
61621
- ctx.fillText(config.brandName, width / 2, textY, width - logoPad * 2);
61623
+ ctx.textBaseline = 'top';
61624
+ ctx.fillText(config.brandName, width / 2, cursorY, width - 48);
61625
+ cursorY += 32;
61626
+ }
61627
+ const metaParts = [];
61628
+ if (config.eventDate)
61629
+ metaParts.push(config.eventDate);
61630
+ if (config.eventLocation)
61631
+ metaParts.push(config.eventLocation);
61632
+ if (metaParts.length > 0) {
61633
+ ctx.fillStyle = 'rgba(255,255,255,0.78)';
61634
+ ctx.font = `0.875rem ${FONT_STACK}`;
61635
+ ctx.textAlign = 'center';
61636
+ ctx.textBaseline = 'top';
61637
+ ctx.fillText(metaParts.join(' · '), width / 2, cursorY, width - 48);
61622
61638
  }
61623
61639
  }
61624
61640
  async drawQrZone(ctx, config, width) {
@@ -61626,7 +61642,7 @@ class TicketCardImageService {
61626
61642
  ctx.fillStyle = '#ffffff';
61627
61643
  ctx.fillRect(0, zoneTop, width, QR_ZONE_H);
61628
61644
  const qrX = (width - QR_SIZE) / 2;
61629
- const qrY = zoneTop + (QR_ZONE_H - QR_SIZE - 28) / 2;
61645
+ const qrY = zoneTop + (QR_ZONE_H - QR_SIZE - 40) / 2;
61630
61646
  try {
61631
61647
  const qrImg = await this.loadImage(config.qrDataUrl);
61632
61648
  ctx.drawImage(qrImg, qrX, qrY, QR_SIZE, QR_SIZE);
@@ -61635,42 +61651,106 @@ class TicketCardImageService {
61635
61651
  ctx.strokeStyle = '#e0e0e0';
61636
61652
  ctx.strokeRect(qrX, qrY, QR_SIZE, QR_SIZE);
61637
61653
  }
61638
- ctx.fillStyle = '#2f2a36';
61639
- ctx.font = `0.9375rem ${FONT_STACK}`;
61654
+ const hint = config.scanHint ?? 'Muestra este QR en la puerta para canjear tu entrada.';
61655
+ ctx.fillStyle = '#6b6675';
61656
+ ctx.font = `0.875rem ${FONT_STACK}`;
61640
61657
  ctx.textAlign = 'center';
61641
61658
  ctx.textBaseline = 'top';
61642
- ctx.fillText('Escanea para verificar', width / 2, qrY + QR_SIZE + 12);
61659
+ const hintY = qrY + QR_SIZE + 14;
61660
+ this.fillWrappedText(ctx, hint, width / 2, hintY, width - 80, 22);
61661
+ }
61662
+ drawPerforated(ctx, config, width) {
61663
+ const y = HEADER_H + QR_ZONE_H;
61664
+ const notchR = PERF_H / 2;
61665
+ const colorFrom = config.brandColor ?? DEFAULT_BRAND_COLOR;
61666
+ ctx.fillStyle = '#ffffff';
61667
+ ctx.fillRect(0, y, width, PERF_H);
61668
+ // Left notch (semicircle cut from edge)
61669
+ ctx.fillStyle = '#f8f8f8';
61670
+ ctx.beginPath();
61671
+ ctx.arc(-1, y + notchR, notchR + 1, -Math.PI / 2, Math.PI / 2);
61672
+ ctx.fill();
61673
+ // Right notch
61674
+ ctx.beginPath();
61675
+ ctx.arc(width + 1, y + notchR, notchR + 1, Math.PI / 2, -Math.PI / 2);
61676
+ ctx.fill();
61677
+ // Dashed line in middle
61678
+ ctx.setLineDash([8, 6]);
61679
+ ctx.strokeStyle = colorFrom + '55';
61680
+ ctx.lineWidth = 1.5;
61681
+ ctx.beginPath();
61682
+ ctx.moveTo(notchR * 2 + 8, y + notchR);
61683
+ ctx.lineTo(width - notchR * 2 - 8, y + notchR);
61684
+ ctx.stroke();
61685
+ ctx.setLineDash([]);
61643
61686
  }
61644
61687
  drawFooter(ctx, config, width) {
61645
- const footerTop = HEADER_H + QR_ZONE_H;
61688
+ const footerTop = HEADER_H + QR_ZONE_H + PERF_H;
61689
+ const colorFrom = config.brandColor ?? DEFAULT_BRAND_COLOR;
61646
61690
  ctx.fillStyle = '#ffffff';
61647
61691
  ctx.fillRect(0, footerTop, width, FOOTER_H);
61648
- const colorFrom = config.brandColor ?? DEFAULT_BRAND_COLOR;
61649
- ctx.fillStyle = colorFrom;
61650
- ctx.fillRect(0, footerTop, width, 3);
61651
- let contentY = footerTop + 40;
61692
+ let contentY = footerTop + 32;
61652
61693
  if (config.folio !== undefined && config.folio !== null && config.folio !== '') {
61653
- const label = config.folioLabel ?? 'Entrada';
61654
- const folioText = `${label} #${config.folio}`;
61655
- ctx.fillStyle = '#1a0b2e';
61656
- ctx.font = `bold 3rem ${FONT_STACK}`;
61694
+ const label = (config.folioLabel ?? 'Cartón').toUpperCase();
61695
+ // Label small brand color, letter-spaced
61696
+ ctx.fillStyle = colorFrom;
61697
+ ctx.font = `600 0.8125rem ${FONT_STACK}`;
61657
61698
  ctx.textAlign = 'center';
61658
61699
  ctx.textBaseline = 'top';
61659
- ctx.fillText(folioText, width / 2, contentY, width - 40);
61660
- contentY += 56;
61700
+ this.fillLetterSpaced(ctx, label, width / 2, contentY, 3);
61701
+ contentY += 24;
61702
+ // Folio number — large, bold, brand color
61703
+ ctx.fillStyle = colorFrom;
61704
+ ctx.font = `800 3.5rem ${FONT_STACK}`;
61705
+ ctx.textAlign = 'center';
61706
+ ctx.textBaseline = 'top';
61707
+ ctx.fillText(`#${config.folio}`, width / 2, contentY, width - 40);
61708
+ contentY += 68;
61661
61709
  }
61662
61710
  if (config.buyerName) {
61663
- ctx.fillStyle = '#2f2a36';
61664
- ctx.font = `1.25rem ${FONT_STACK}`;
61711
+ ctx.fillStyle = '#6b6675';
61712
+ ctx.font = `0.9375rem ${FONT_STACK}`;
61665
61713
  ctx.textAlign = 'center';
61666
61714
  ctx.textBaseline = 'top';
61667
61715
  ctx.fillText(config.buyerName, width / 2, contentY, width - 40);
61668
61716
  }
61669
- ctx.fillStyle = '#6b6675';
61670
- ctx.font = `0.75rem ${FONT_STACK}`;
61671
- ctx.textAlign = 'right';
61717
+ // Copyright
61718
+ ctx.fillStyle = '#b0a8bc';
61719
+ ctx.font = `0.6875rem ${FONT_STACK}`;
61720
+ ctx.textAlign = 'center';
61672
61721
  ctx.textBaseline = 'bottom';
61673
- ctx.fillText('Valtech', width - 16, footerTop + FOOTER_H - 12);
61722
+ ctx.fillText('© 2026 Valtech. Todos los derechos reservados.', width / 2, footerTop + FOOTER_H - 14);
61723
+ }
61724
+ fillWrappedText(ctx, text, x, y, maxWidth, lineHeight) {
61725
+ const words = text.split(' ');
61726
+ let line = '';
61727
+ let curY = y;
61728
+ for (const word of words) {
61729
+ const test = line ? `${line} ${word}` : word;
61730
+ if (ctx.measureText(test).width > maxWidth && line) {
61731
+ ctx.fillText(line, x, curY);
61732
+ line = word;
61733
+ curY += lineHeight;
61734
+ }
61735
+ else {
61736
+ line = test;
61737
+ }
61738
+ }
61739
+ if (line)
61740
+ ctx.fillText(line, x, curY);
61741
+ }
61742
+ fillLetterSpaced(ctx, text, cx, y, spacing) {
61743
+ // Measure total width with spacing and center
61744
+ const chars = text.split('');
61745
+ const charWidths = chars.map(c => ctx.measureText(c).width);
61746
+ const totalW = charWidths.reduce((s, w) => s + w, 0) + spacing * (chars.length - 1);
61747
+ let x = cx - totalW / 2;
61748
+ ctx.textAlign = 'left';
61749
+ for (let i = 0; i < chars.length; i++) {
61750
+ ctx.fillText(chars[i], x, y);
61751
+ x += charWidths[i] + spacing;
61752
+ }
61753
+ ctx.textAlign = 'center';
61674
61754
  }
61675
61755
  loadImage(src) {
61676
61756
  return new Promise((resolve, reject) => {
@@ -70432,6 +70512,9 @@ class TicketCardComponent {
70432
70512
  folio: p.folio,
70433
70513
  folioLabel: p.shareFolioLabel,
70434
70514
  buyerName: p.buyerName,
70515
+ eventDate: p.shareEventDate,
70516
+ eventLocation: p.shareEventLocation,
70517
+ scanHint: p.shareScanHint,
70435
70518
  };
70436
70519
  }
70437
70520
  async copyToClipboard(config, p) {