@jaak.ai/stamps 2.1.0-dev.1 → 2.1.0-dev.3

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.
Files changed (30) hide show
  1. package/README.md +11 -1
  2. package/dist/cjs/jaak-stamps-webcomponent.cjs.js +1 -1
  3. package/dist/cjs/jaak-stamps.cjs.entry.js +225 -18
  4. package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
  5. package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
  6. package/dist/cjs/loader.cjs.js +1 -1
  7. package/dist/collection/components/my-component/my-component.css +164 -9
  8. package/dist/collection/components/my-component/my-component.js +230 -16
  9. package/dist/collection/components/my-component/my-component.js.map +1 -1
  10. package/dist/collection/services/DetectionService.js +16 -1
  11. package/dist/collection/services/DetectionService.js.map +1 -1
  12. package/dist/collection/services/ServiceContainer.js +3 -2
  13. package/dist/collection/services/ServiceContainer.js.map +1 -1
  14. package/dist/components/jaak-stamps.js +228 -18
  15. package/dist/components/jaak-stamps.js.map +1 -1
  16. package/dist/esm/jaak-stamps-webcomponent.js +1 -1
  17. package/dist/esm/jaak-stamps.entry.js +225 -18
  18. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  19. package/dist/esm/loader.js +1 -1
  20. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  21. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  22. package/dist/jaak-stamps-webcomponent/p-4b2f9fb3.entry.js +2 -0
  23. package/dist/jaak-stamps-webcomponent/p-4b2f9fb3.entry.js.map +1 -0
  24. package/dist/types/components/my-component/my-component.d.ts +8 -0
  25. package/dist/types/components.d.ts +8 -0
  26. package/dist/types/services/DetectionService.d.ts +2 -1
  27. package/dist/types/services/ServiceContainer.d.ts +1 -0
  28. package/package.json +1 -1
  29. package/dist/jaak-stamps-webcomponent/p-39560f23.entry.js +0 -2
  30. package/dist/jaak-stamps-webcomponent/p-39560f23.entry.js.map +0 -1
@@ -5,7 +5,7 @@ var index = require('./index-BfhtOB0D.js');
5
5
  const defineCustomElements = async (win, options) => {
6
6
  if (typeof window === 'undefined') return undefined;
7
7
  await index.globalScripts();
8
- return index.bootstrapLazy([["jaak-stamps.cjs",[[1,"jaak-stamps",{"debug":[4],"alignmentTolerance":[2,"alignment-tolerance"],"maskSize":[2,"mask-size"],"cropMargin":[2,"crop-margin"],"useDocumentClassification":[4,"use-document-classification"],"preferredCamera":[1,"preferred-camera"],"captureDelay":[2,"capture-delay"],"enableBackDocumentTimer":[4,"enable-back-document-timer"],"backDocumentTimerDuration":[2,"back-document-timer-duration"],"detectionBoxes":[32],"sideAlignment":[32],"isMaskReady":[32],"shouldMirrorVideo":[32],"showCameraSelector":[32],"isSwitchingCamera":[32],"hasDocumentDetected":[32],"cameraInfoWithAutofocus":[32],"currentStatus":[32],"performanceData":[32],"backDocumentTimerRemaining":[32],"getCapturedImages":[64],"isProcessCompleted":[64],"startCapture":[64],"stopCapture":[64],"resetCapture":[64],"skipBackCapture":[64],"getStatus":[64],"preloadModel":[64],"getCameraInfo":[64],"setPreferredCamera":[64],"setCaptureDelay":[64],"getCaptureDelay":[64]}]]]], options);
8
+ return index.bootstrapLazy([["jaak-stamps.cjs",[[1,"jaak-stamps",{"debug":[4],"alignmentTolerance":[2,"alignment-tolerance"],"maskSize":[2,"mask-size"],"cropMargin":[2,"crop-margin"],"useDocumentClassification":[4,"use-document-classification"],"useDocumentDetector":[4,"use-document-detector"],"preferredCamera":[1,"preferred-camera"],"captureDelay":[2,"capture-delay"],"enableBackDocumentTimer":[4,"enable-back-document-timer"],"backDocumentTimerDuration":[2,"back-document-timer-duration"],"detectionBoxes":[32],"sideAlignment":[32],"isMaskReady":[32],"shouldMirrorVideo":[32],"showCameraSelector":[32],"isSwitchingCamera":[32],"hasDocumentDetected":[32],"cameraInfoWithAutofocus":[32],"currentStatus":[32],"performanceData":[32],"backDocumentTimerRemaining":[32],"showManualCaptureButton":[32],"captureStateVersion":[32],"getCapturedImages":[64],"isProcessCompleted":[64],"startCapture":[64],"stopCapture":[64],"resetCapture":[64],"skipBackCapture":[64],"getStatus":[64],"preloadModel":[64],"getCameraInfo":[64],"setPreferredCamera":[64],"setCaptureDelay":[64],"getCaptureDelay":[64]}]]]], options);
9
9
  };
10
10
 
11
11
  exports.setNonce = index.setNonce;
@@ -170,18 +170,21 @@ video {
170
170
  top: 50%;
171
171
  left: 50%;
172
172
  transform: translate(-50%, -50%);
173
- color: #fff;
174
- font-size: 14px;
175
- font-weight: 600;
173
+ background: rgba(0, 0, 0, 0.25);
174
+ backdrop-filter: blur(20px);
175
+ border-radius: 12px;
176
+ border: 1px solid rgba(255, 255, 255, 0.1);
177
+ color: #ffffff;
178
+ font-size: 12px;
179
+ font-weight: 500;
176
180
  text-align: center;
177
181
  white-space: normal;
178
- background: rgba(0, 0, 0, 0.25);
179
- backdrop-filter: blur(12px);
180
- padding: 12px 20px;
181
- border-radius: 8px;
182
+ padding: 13px;
182
183
  max-width: 300px;
183
184
  z-index: 20;
184
- border: 1px solid rgba(255, 255, 255, 0.1);
185
+ height: fit-content;
186
+ width: fit-content;
187
+ animation: slideInFromTopCentered 0.3s ease-out;
185
188
  }
186
189
 
187
190
  /* Quality indicator */
@@ -523,6 +526,81 @@ video {
523
526
  max-width: 300px;
524
527
  }
525
528
 
529
+ /* Sección unificada para captura del reverso */
530
+ .back-capture-section {
531
+ position: absolute;
532
+ bottom: 20px;
533
+ left: 50%;
534
+ transform: translateX(-50%);
535
+ z-index: 25;
536
+ display: flex;
537
+ flex-direction: column;
538
+ align-items: center;
539
+ width: 100%;
540
+ max-width: 320px;
541
+ }
542
+
543
+
544
+ .back-capture-buttons {
545
+ display: flex;
546
+ gap: 12px;
547
+ align-items: center;
548
+ justify-content: center;
549
+ flex-wrap: wrap;
550
+ }
551
+
552
+ .capture-button {
553
+ pointer-events: auto;
554
+ background: #fff;
555
+ color: #333;
556
+ border: none;
557
+ border-radius: 25px;
558
+ padding: 12px 24px;
559
+ font-size: 14px;
560
+ font-weight: 600;
561
+ cursor: pointer;
562
+ transition: all 0.3s ease;
563
+ }
564
+
565
+ .capture-button:hover {
566
+ background: #f8f9fa;
567
+ }
568
+
569
+ .capture-button:active {
570
+ background: #e9ecef;
571
+ transform: translateY(1px);
572
+ }
573
+
574
+ .capture-button:disabled {
575
+ background: #e9ecef;
576
+ color: #6c757d;
577
+ cursor: not-allowed;
578
+ transform: none;
579
+ }
580
+
581
+ /* Responsive para móviles */
582
+ @media (max-width: 480px) {
583
+ .back-capture-section {
584
+ bottom: 16px;
585
+ max-width: 300px;
586
+ }
587
+
588
+
589
+ .back-capture-buttons {
590
+ flex-direction: column;
591
+ gap: 8px;
592
+ width: 100%;
593
+ }
594
+
595
+ .capture-button,
596
+ .back-capture-buttons .skip-button {
597
+ width: 100%;
598
+ max-width: 200px;
599
+ padding: 10px 20px;
600
+ font-size: 13px;
601
+ }
602
+ }
603
+
526
604
  .skip-explanation {
527
605
  background: rgba(0, 0, 0, 0.5);
528
606
  color: #ffffff;
@@ -575,6 +653,7 @@ video {
575
653
  display: flex;
576
654
  gap: 8px;
577
655
  pointer-events: auto;
656
+ animation: slideInFromTop 0.3s ease-out;
578
657
  }
579
658
 
580
659
 
@@ -665,6 +744,18 @@ video {
665
744
  }
666
745
  }
667
746
 
747
+ /* Animación específica para elementos centrados */
748
+ @keyframes slideInFromTopCentered {
749
+ 0% {
750
+ opacity: 0;
751
+ transform: translate(-50%, -50%) translateY(-8px) scale(0.95);
752
+ }
753
+ 100% {
754
+ opacity: 1;
755
+ transform: translate(-50%, -50%) translateY(0) scale(1);
756
+ }
757
+ }
758
+
668
759
  .camera-selector-header {
669
760
  display: flex;
670
761
  justify-content: space-between;
@@ -903,6 +994,12 @@ video {
903
994
  .status-description {
904
995
  font-size: 9px;
905
996
  }
997
+
998
+ .guide-text {
999
+ font-size: 11px;
1000
+ padding: 4px 8px;
1001
+ border-radius: 8px;
1002
+ }
906
1003
  }
907
1004
 
908
1005
  /* Animación de carga mejorada */
@@ -968,7 +1065,7 @@ video {
968
1065
  border: 1px solid rgba(255, 255, 255, 0.1);
969
1066
  z-index: 35;
970
1067
  width: fit-content;
971
- animation: slideInFromLeft 0.3s ease-out;
1068
+ animation: slideInFromTop 0.3s ease-out;
972
1069
  transition: all 0.3s ease;
973
1070
  }
974
1071
 
@@ -1127,3 +1224,61 @@ video {
1127
1224
  font-size: 10px;
1128
1225
  }
1129
1226
  }
1227
+
1228
+ /* Sección de captura manual */
1229
+ .manual-capture-section {
1230
+ position: absolute;
1231
+ bottom: 20px;
1232
+ left: 50%;
1233
+ transform: translateX(-50%);
1234
+ z-index: 25;
1235
+ display: flex;
1236
+ flex-direction: column;
1237
+ align-items: center;
1238
+ width: 100%;
1239
+ max-width: 300px;
1240
+ }
1241
+
1242
+
1243
+ .manual-capture-button {
1244
+ pointer-events: auto;
1245
+ background: #fff;
1246
+ color: #333;
1247
+ border: none;
1248
+ border-radius: 25px;
1249
+ padding: 12px 24px;
1250
+ font-size: 14px;
1251
+ font-weight: 600;
1252
+ cursor: pointer;
1253
+ transition: all 0.3s ease;
1254
+ }
1255
+
1256
+ .manual-capture-button:hover {
1257
+ background: #f8f9fa;
1258
+ }
1259
+
1260
+ .manual-capture-button:active {
1261
+ background: #e9ecef;
1262
+ transform: translateY(1px);
1263
+ }
1264
+
1265
+ .manual-capture-button:disabled {
1266
+ background: #e9ecef;
1267
+ color: #6c757d;
1268
+ cursor: not-allowed;
1269
+ transform: none;
1270
+ }
1271
+
1272
+ /* Responsive para móviles */
1273
+ @media (max-width: 480px) {
1274
+ .manual-capture-section {
1275
+ bottom: 16px;
1276
+ max-width: 280px;
1277
+ }
1278
+
1279
+
1280
+ .manual-capture-button {
1281
+ padding: 10px 20px;
1282
+ font-size: 13px;
1283
+ }
1284
+ }
@@ -7,6 +7,7 @@ export class JaakStamps {
7
7
  maskSize = 90;
8
8
  cropMargin = 20;
9
9
  useDocumentClassification = false;
10
+ useDocumentDetector = true;
10
11
  preferredCamera = 'auto';
11
12
  captureDelay = 1500;
12
13
  enableBackDocumentTimer = false;
@@ -47,6 +48,8 @@ export class JaakStamps {
47
48
  detectionRate: 0
48
49
  };
49
50
  backDocumentTimerRemaining = 0;
51
+ showManualCaptureButton = false;
52
+ captureStateVersion = 0; // Triggers re-render when StateManager changes
50
53
  // Services
51
54
  serviceContainer;
52
55
  eventBus;
@@ -110,6 +113,7 @@ export class JaakStamps {
110
113
  maskSize: this.maskSize,
111
114
  cropMargin: this.cropMargin,
112
115
  useDocumentClassification: this.useDocumentClassification,
116
+ useDocumentDetector: this.useDocumentDetector,
113
117
  preferredCamera: this.preferredCamera,
114
118
  captureDelay: this.captureDelay
115
119
  };
@@ -167,27 +171,39 @@ export class JaakStamps {
167
171
  document.head.appendChild(script);
168
172
  await new Promise((resolve) => {
169
173
  script.onload = () => {
170
- setTimeout(() => {
171
- this.finalizeInitialization();
174
+ setTimeout(async () => {
175
+ await this.finalizeInitialization();
172
176
  resolve(undefined);
173
177
  }, 300);
174
178
  };
175
179
  });
176
180
  }
177
181
  else {
178
- setTimeout(() => {
179
- this.finalizeInitialization();
182
+ setTimeout(async () => {
183
+ await this.finalizeInitialization();
180
184
  }, 300);
181
185
  }
182
186
  }
183
- finalizeInitialization() {
187
+ async finalizeInitialization() {
184
188
  this.stateManager.updateCaptureState({ isLoading: false });
185
- this.currentStatus = {
186
- message: 'Listo para capturar',
187
- description: '',
188
- type: 'ready',
189
- isInitialized: true
190
- };
189
+ // Check camera permissions before showing "ready" status
190
+ const hasPermissions = await this.checkCameraPermissions();
191
+ if (hasPermissions) {
192
+ this.currentStatus = {
193
+ message: 'Listo para capturar',
194
+ description: '',
195
+ type: 'ready',
196
+ isInitialized: true
197
+ };
198
+ }
199
+ else {
200
+ this.currentStatus = {
201
+ message: 'Permisos de cámara requeridos',
202
+ description: 'Por favor, otorgue permisos de cámara para continuar',
203
+ type: 'error',
204
+ isInitialized: true
205
+ };
206
+ }
191
207
  this.emitReadyEvent();
192
208
  }
193
209
  updateStatus(message, description, type = 'loading') {
@@ -202,6 +218,26 @@ export class JaakStamps {
202
218
  const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
203
219
  this.isReady.emit(isDocumentReady);
204
220
  }
221
+ async checkCameraPermissions() {
222
+ try {
223
+ if (!navigator.permissions) {
224
+ // Fallback: try to access camera directly
225
+ try {
226
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
227
+ stream.getTracks().forEach(track => track.stop());
228
+ return true;
229
+ }
230
+ catch {
231
+ return false;
232
+ }
233
+ }
234
+ const permission = await navigator.permissions.query({ name: 'camera' });
235
+ return permission.state === 'granted';
236
+ }
237
+ catch (error) {
238
+ return false;
239
+ }
240
+ }
205
241
  handleStateChange(data) {
206
242
  // React to state changes from StateManager
207
243
  // This is where the component updates its internal state based on service state changes
@@ -225,6 +261,32 @@ export class JaakStamps {
225
261
  this.updateMaskDimensions(rect);
226
262
  }
227
263
  }
264
+ triggerRerender() {
265
+ this.captureStateVersion = this.captureStateVersion + 1;
266
+ }
267
+ getGuideText(step) {
268
+ if (step === 'completed') {
269
+ return 'Proceso completado';
270
+ }
271
+ if (step === 'front') {
272
+ // Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
273
+ if (this.showManualCaptureButton) {
274
+ return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
275
+ }
276
+ // Instrucción básica de alineación
277
+ return 'Alinee su identificación con el marco';
278
+ }
279
+ else if (step === 'back') {
280
+ // Para el reverso, mostrar instrucciones específicas según el modo
281
+ if (this.useDocumentDetector) {
282
+ return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
283
+ }
284
+ else {
285
+ return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
286
+ }
287
+ }
288
+ return 'Alinee su identificación con el marco';
289
+ }
228
290
  updateMaskDimensions(containerRect) {
229
291
  if (!this.videoRef)
230
292
  return;
@@ -572,7 +634,12 @@ export class JaakStamps {
572
634
  this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
573
635
  const stream = await this.cameraService.setupCamera();
574
636
  // Paso 4: Inicializar video y ocultar estado inmediatamente
575
- this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
637
+ if (this.useDocumentDetector) {
638
+ this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
639
+ }
640
+ else {
641
+ this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
642
+ }
576
643
  await this.initializeVideoStream(stream);
577
644
  // Paso 5: Calibrar máscara
578
645
  if (this.detectionContainer) {
@@ -583,6 +650,10 @@ export class JaakStamps {
583
650
  // Finalizar e iniciar captura
584
651
  this.startTime = Date.now();
585
652
  this.stateManager.updateCaptureState({ isLoading: false });
653
+ // Mostrar botón manual si el detector está deshabilitado
654
+ if (!this.useDocumentDetector) {
655
+ this.showManualCaptureButton = true;
656
+ }
586
657
  this.detectFrame();
587
658
  }
588
659
  catch (err) {
@@ -614,6 +685,18 @@ export class JaakStamps {
614
685
  const captureState = this.stateManager.getCaptureState();
615
686
  if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
616
687
  return;
688
+ // Show manual capture button when document detector is disabled
689
+ if (!this.useDocumentDetector) {
690
+ this.showManualCaptureButton = true;
691
+ // Continue the loop for UI updates but skip detection logic
692
+ if (captureState.step !== 'completed') {
693
+ this.animationId = requestAnimationFrame(() => this.detectFrame());
694
+ }
695
+ return;
696
+ }
697
+ else {
698
+ this.showManualCaptureButton = false;
699
+ }
617
700
  if (captureState.isDetectionPaused) {
618
701
  if (captureState.step !== 'completed') {
619
702
  this.animationId = requestAnimationFrame(() => this.detectFrame());
@@ -742,7 +825,7 @@ export class JaakStamps {
742
825
  isCapturing: false
743
826
  };
744
827
  const cameraInfo = this.cameraInfoWithAutofocus;
745
- return (h("div", { key: '69a8df7969288e19088e8809f1fefcc89b237cda', class: "detector-container" }, h("div", { key: '79e59a315d266be9d4fb5eed5740e5ad825d5890', class: "video-container" }, h("video", { key: 'a3312788c739748b774a25acb30c2dcea7f01695', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: 'ddab107500c2dab24c140049bef0b44aafc35dc7', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
828
+ return (h("div", { key: '3026fb421368ee82a1d2cefd8f9355ebf3194146', class: "detector-container" }, h("div", { key: '62b1230c3268412f4ff301d480d666065037d9b3', class: "video-container" }, h("video", { key: '170a0e7235522dec33b914b6d2841b36503c781f', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '915af3649a7a551a46f1cb97af0a45916666cc12', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
746
829
  position: 'absolute',
747
830
  left: `${box.x}px`,
748
831
  top: `${box.y}px`,
@@ -751,9 +834,9 @@ export class JaakStamps {
751
834
  border: '2px solid #32406C',
752
835
  pointerEvents: 'none',
753
836
  boxSizing: 'border-box'
754
- } })))), this.isMaskReady && (h("div", { key: '370337089a3de949b8cb4ae84dc0037e6044b2d5', class: "overlay-mask" }, h("div", { key: 'cb35c2e42bf1729ad02a8fe7e7abaf27cf1a8d7a', class: "card-outline" }, h("div", { key: '637d72ada63805e7f25051cd1ad9093030cb025a', class: "side side-top" }), h("div", { key: 'c7a465aa686afa41f80433d949715b40f6f349dc', class: "side side-right" }), h("div", { key: '384aade0610b5bc7bdd7db350a70d82f9bebd8ab', class: "side side-bottom" }), h("div", { key: '5506cc346f3fd718e3fda4365f69ea780304b6d6', class: "side side-left" }), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'f5b9b62937eabab7c096b7417a13bbf6f822ee8a', class: "guide-text" }, "Alinee su identificaci\u00F3n con el marco"))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '74b122125a42839d2600cf99fb855140042ab761', class: "skip-section" }, h("div", { key: '0b4a39d1599854d6c445e2c35dcc89a0e4970846', class: "skip-explanation" }, "Si tu documento no tiene lado trasero, da clic en el bot\u00F3n para continuar con el proceso"), h("button", { key: 'b52b47df390cc099c8847ca8a676270bb154294d', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
837
+ } })))), this.isMaskReady && (h("div", { key: 'b40cf136e5b9a120aa7243c12005383196428b34', class: "overlay-mask" }, h("div", { key: 'e82324a37682c94f03576e89d880c3b17520b5e5', class: "card-outline" }, h("div", { key: 'b1fd7df2a36300f2e6eb96474b449be74abd1ed4', class: "side side-top" }), h("div", { key: 'a6b3eb63fb35b22d0d53c8fd5aa88b2e299ae8c4', class: "side side-right" }), h("div", { key: '2d43e9281ad4fe05e314627aa249f56a7b0de9f1', class: "side side-bottom" }), h("div", { key: '231440a2dccc0f8ede35971ef092523d7be444b9', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'a923e81e11ead323b9bcf76bd6b0fea421bc1517', class: "guide-text" }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '4b68212634cbc4916cd16c4e00ca4e91b924de6e', class: "back-capture-section" }, h("div", { key: 'f2ec22b132defe1e4026ad38977c52974c5cdb21', class: "back-capture-buttons" }, !this.useDocumentDetector && (h("button", { key: 'ae8c0e452fbf1bb69b6683ae775f1c1a4b7d22d9', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Reverso')), h("button", { key: 'f1ec52715a584da161885bcbae0db7a38632905c', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
755
838
  ? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
756
- : 'Saltar reverso'))), captureState.isVideoActive && (h("div", { key: 'f04e47749a25197c434b849844c67eadca5c6af3', class: "camera-controls" }, h("button", { key: '82625eef057f59b6dac11c4443ccb315231eac7f', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (h("div", { key: '61cfb89cd9bb549cfff2cb70293db06ee6cdf06b', class: "camera-selector-dropdown" }, h("div", { key: '60f751d8db7b305f2bdfdc089d2110bcff940150', class: "camera-selector-header" }, h("span", { key: '9dae045a34cc06f323ff4850921b495fc845be47' }, "Seleccionar C\u00E1mara"), h("button", { key: '4584d7964d91ddd7f3d980c3e5e90493530d33bd', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '9881f2b4fc6ffbdc76db06a7b49d060cefbe6277', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '3dfe9ffc7b4cc9afb199a78026cef6d4b469805e', class: "device-info" }, h("small", { key: 'bddc865a30469bd2724624b2f7c8f641f9d97a07' }, "Dispositivo: ", cameraInfo.deviceType)))))), captureState.isCapturing && (h("div", { key: '2fd963e44f69f80d17c6bbbfa3c9b5e11bc816be', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: 'ba4872ea5388a0ac3134be61feff5412373465ab', class: "flip-animation" }, h("div", { key: 'e83a92cea79f3e85aefc6964c5d9de7b495d511e', class: "id-card-icon" }), h("div", { key: '1d06b51fd6ad14d9b37aa6bca72bd13c951a386a', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '503c377896d5fdc3ae8476b09f511d571f23ae72', class: "success-animation" }, h("div", { key: 'e1929c245b0d1479a6fb1513b0b37c53070c4291', class: "check-icon" }), h("div", { key: '024c897c70427b205390770fc3dfb999ffcc84d3', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '54e350707628e5c88ae2900e412e6bda2eace943', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: '82ea10569ed46285686cf3e63885ca1941bb559c', class: "status-spinner" })), h("div", { key: '1fecf179c10965cccf1be3dc0a54afd377ba38ad', class: "status-content" }, h("div", { key: '1b301c1bbb02e5cac2e6babcb20c190662d45c68', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: '5b6c61308d17176daade02c951a436d114a74100', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'b8104db64208dc87a617a072957c007f28055194', class: "performance-monitor" }, h("div", { key: 'e66aec071f10821be2713858dcc251c04f480e63', class: "performance-expanded" }, h("div", { key: '1ae09a91d0704ab21b9eb51097cf452fd5f44359', class: "metrics-row" }, h("div", { key: '340d4d16d048a5217fe13d5de63618c9ea221165', class: "metric-compact" }, h("span", { key: '20cfda14969ef70d232edd68f2e9902bd44b5d9a', class: "metric-label" }, "FPS"), h("span", { key: '3a089c2814a71ed6513bf05b0120a66aa159daee', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: '23f700def988c3220fc5df37aeb57a0a20ff56b3', class: "metric-compact" }, h("span", { key: '441e8490a871469813a6d832114e03583d375e91', class: "metric-label" }, "MEM"), h("span", { key: '51f7c49164c9d5e699141c92c88375fc99b312ed', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: '6547fee80e2a1db7c9ff889a1c478fc8248159e6', class: "metrics-row" }, h("div", { key: '7b2d1642fd122ee0fbed36387afb23152afe10c2', class: "metric-compact" }, h("span", { key: 'a9137119bf7da366f18feaf8f28807ee40ad7a25', class: "metric-label" }, "INF"), h("span", { key: '5cbc0c040102688f1039cdcd4bb58020248cf700', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: 'cb4a86f5da46e4081d997c8b8bb6a899f06dfcd2', class: "metric-compact" }, h("span", { key: '88ad5e1d842eb156cf6813ecaa9d2bec76a5999e', class: "metric-label" }, "FRAME"), h("span", { key: 'a9b14cd98b64dcfc058d3b68a3c168a9e092fb3c', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '86e01f3596682cb8ad2ff5464748e6ecb7db374a', class: "metrics-row" }, h("div", { key: 'e73b9d39e9bcbc5d46fa0ba0a737700e3e5566d9', class: "metric-compact" }, h("span", { key: '8ecfe969dfe77c87d9da76d7390706014b4872f3', class: "metric-label" }, "DET"), h("span", { key: 'bd1a6a52e0bd1fb4d377413d7515e4e0f00da10f', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: '36539ae98787fd60642082db1bf89031003c4bef', class: "metric-compact" }, h("span", { key: '7f3a81629c09053e3beeca97f485da0f60412052', class: "metric-label" }, "RATE"), h("span", { key: '72399e0ac39270c82b265ce64e402f84cda9fb2c', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '4e3a914ba2f5993f85de7b9dd6dc93d76b41c96d', class: "watermark" }, h("img", { key: 'aa634c2ba2090f64ee9b41aa1d75a9114d46fd63', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
839
+ : 'Saltar reverso')))), captureState.isVideoActive && (h("div", { key: 'a831afce8ac493fe9716fc5608c1be5373cac9f4', class: "camera-controls" }, h("button", { key: '9a56a494d6357eac86211af857b74db4509aad64', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (h("div", { key: 'b08699bf97018b3318771aa9c19a5848fae6e09f', class: "camera-selector-dropdown" }, h("div", { key: '24d010fde00991a80f92803511ae31ab23d2d4ee', class: "camera-selector-header" }, h("span", { key: 'ea0a21c5989cc28dff59bab870cfbb39b401b466' }, "Seleccionar C\u00E1mara"), h("button", { key: '2cbd997565bfaeceb799a95fcb8e26681bab22ef', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: 'c00abb13cb7299ff325b85d3fcd44dcad7839e53', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: 'e369c9ae07f0638b490d67bb01ad2a3a7f9b56a1', class: "device-info" }, h("small", { key: '04d4a8c24a1a4487939f87e8af240962705fd685' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'f955e8e5b6917d01477fd171e3c16a44a3d0a916', class: "manual-capture-section" }, h("button", { key: 'cb122ec84fec7afae8185e561f4da197a210b0dd', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Frente'))))), captureState.isCapturing && (h("div", { key: '98d15bcd0afab9daa3bc64096535db74f795723b', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: '86132c96fc3ff3296c04c32c03ef624511d175fd', class: "flip-animation" }, h("div", { key: '05efba20dc5a4890a6de5650e67b5144853fa9d6', class: "id-card-icon" }), h("div", { key: 'a2e307b126a1c65681733e68bb737ed21e9cb119', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '38e1e4f5c3c8a5002fc45c8490e3134e31b4652a', class: "success-animation" }, h("div", { key: '2f6faebde51e90059106e64ef50af51d9395c0cf', class: "check-icon" }), h("div", { key: '63385f75ab3933966f58bb954c13f4cacb4d5c21', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '0853a33e794b26c727336fb13c9430d99b992d7f', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: 'e0d258b6897ebaa281473ee6633244a89d9b3cc2', class: "status-spinner" })), h("div", { key: 'e54b82a5a895734bdaeb95fc9bfb631fd4680c87', class: "status-content" }, h("div", { key: '57e712d858574138a9ffdefa298adc99765788f3', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'b2e1240eb71ed506157353fe5e24d62c3a0da8ab', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: '3f3acb1ffc8e2c1d64cc8dd0e76bf64772a92def', class: "performance-monitor" }, h("div", { key: '83c1640404aad9552ad082a78327acf9facd79ad', class: "performance-expanded" }, h("div", { key: '18ee837b0fa64bae25285183cbf79733f7654153', class: "metrics-row" }, h("div", { key: 'b7d6b1c15b7e57e67d886c1bbc279573e795a3a3', class: "metric-compact" }, h("span", { key: '2b02e2323aadc064231ab8735a5810acc7bdd871', class: "metric-label" }, "FPS"), h("span", { key: 'd0cbe3e212f216834127456574eef6e61b4f3d6c', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: '974538f93bbe0abb25eadba4a73d9a28f9c9ae0a', class: "metric-compact" }, h("span", { key: 'e04bc2180ede406b787f63f92eb6086d4e372c77', class: "metric-label" }, "MEM"), h("span", { key: 'c7911542d7a39a334b0a2f0f258256d11b297fa4', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: '92bbce4d85240a84d53afce554792ae51597e16f', class: "metrics-row" }, h("div", { key: '7b78afc87a85a229b582d870e85d3015ca54b941', class: "metric-compact" }, h("span", { key: '7579b2b804c8a869894c0ed5a89c71f19d493c69', class: "metric-label" }, "INF"), h("span", { key: '029733bca2504d1d3c1fef77519e0df90b28d5d1', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: 'f7fe505877d8e9214c2c819e714e3039c95c0ec7', class: "metric-compact" }, h("span", { key: 'ecef2280e78aecb7b811f9fa14c7fdbda8c4d065', class: "metric-label" }, "FRAME"), h("span", { key: 'c6cadf43725c125f371d3d849e8b09953a3747fc', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '832e8ee75377791a17279a3c7eee12fc45f21820', class: "metrics-row" }, h("div", { key: 'abb2dc9ce8a52374bc04e4b36560885427303fb6', class: "metric-compact" }, h("span", { key: '8f8c9cfab162a01807d07f44278f4f5889a1b206', class: "metric-label" }, "DET"), h("span", { key: '7359af4c236091e99b1f653ba20d8e3bd3acaf1b', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: '7dd17d6d47289a11087a33c6104269899b72e46e', class: "metric-compact" }, h("span", { key: '0b1317becd3af6804e1fb1965593882886c51b15', class: "metric-label" }, "RATE"), h("span", { key: '9e94ba4257aa707e6c751852db48a17fb7ec8bae', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '77beb6f47153c43fddbc643fe00d1777ecfab295', class: "watermark" }, h("img", { key: 'e6aec0055f8ffb058dc2249fa1182dae06638a31', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
757
840
  }
758
841
  // Utility methods
759
842
  updateDetectionBoxes(boxes) {
@@ -868,6 +951,115 @@ export class JaakStamps {
868
951
  }
869
952
  }
870
953
  }
954
+ async takeManualScreenshot() {
955
+ if (!this.videoRef)
956
+ return;
957
+ // When using manual capture, use mask coordinates for cropping
958
+ await this.takeScreenshotWithMaskCoordinates();
959
+ }
960
+ async takeScreenshotWithMaskCoordinates() {
961
+ if (!this.videoRef || !this.detectionContainer)
962
+ return;
963
+ this.stateManager.updateCaptureState({ isCapturing: true });
964
+ this.triggerCaptureAnimation();
965
+ // Use pooled canvas for optimization
966
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
967
+ const captureCtx = captureCanvas.getContext('2d', { alpha: false });
968
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
969
+ captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
970
+ // Calculate mask coordinates for cropping
971
+ const container = this.detectionContainer.parentElement;
972
+ const containerRect = container.getBoundingClientRect();
973
+ // Get mask dimensions from CSS properties
974
+ const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
975
+ const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
976
+ const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
977
+ const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
978
+ // Convert percentages to actual video coordinates
979
+ const videoWidth = this.videoRef.videoWidth;
980
+ const videoHeight = this.videoRef.videoHeight;
981
+ const videoAspectRatio = videoWidth / videoHeight;
982
+ const containerAspectRatio = containerRect.width / containerRect.height;
983
+ let displayedVideoWidth, displayedVideoHeight;
984
+ let videoOffsetX = 0, videoOffsetY = 0;
985
+ if (videoAspectRatio > containerAspectRatio) {
986
+ displayedVideoWidth = containerRect.width;
987
+ displayedVideoHeight = containerRect.width / videoAspectRatio;
988
+ videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
989
+ }
990
+ else {
991
+ displayedVideoHeight = containerRect.height;
992
+ displayedVideoWidth = containerRect.height * videoAspectRatio;
993
+ videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
994
+ }
995
+ // Calculate mask coordinates in video space
996
+ const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
997
+ const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
998
+ const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
999
+ const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
1000
+ // Convert to video coordinates
1001
+ const scaleX = videoWidth / displayedVideoWidth;
1002
+ const scaleY = videoHeight / displayedVideoHeight;
1003
+ const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
1004
+ const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
1005
+ const maskWidthInVideo = maskWidthInContainer * scaleX;
1006
+ const maskHeightInVideo = maskHeightInContainer * scaleY;
1007
+ // Calculate crop coordinates
1008
+ const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
1009
+ const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
1010
+ const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
1011
+ const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
1012
+ // Use pooled canvas for cropped version
1013
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
1014
+ const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
1015
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
1016
+ croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
1017
+ const captureState = this.stateManager.getCaptureState();
1018
+ if (captureState.step === 'front') {
1019
+ this.stateManager.setCapturedImages({
1020
+ front: {
1021
+ fullFrame: captureCanvas.toDataURL('image/png'),
1022
+ cropped: croppedCanvas.toDataURL('image/png')
1023
+ }
1024
+ });
1025
+ // Check if document classification is enabled (independent of detector)
1026
+ if (this.useDocumentClassification) {
1027
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
1028
+ if (classification && classification.class === 'passport') {
1029
+ this.completeProcess(true);
1030
+ this.returnCanvasToPool(captureCanvas);
1031
+ this.returnCanvasToPool(croppedCanvas);
1032
+ return;
1033
+ }
1034
+ }
1035
+ this.stateManager.updateCaptureState({
1036
+ step: 'back',
1037
+ isDetectionPaused: true,
1038
+ showFlipAnimation: true
1039
+ });
1040
+ this.triggerRerender();
1041
+ setTimeout(() => {
1042
+ this.stateManager.updateCaptureState({
1043
+ showFlipAnimation: false,
1044
+ isDetectionPaused: false
1045
+ });
1046
+ this.triggerRerender();
1047
+ this.startBackDocumentTimer();
1048
+ }, 3000);
1049
+ }
1050
+ else if (captureState.step === 'back') {
1051
+ this.stateManager.setCapturedImages({
1052
+ back: {
1053
+ fullFrame: captureCanvas.toDataURL('image/png'),
1054
+ cropped: croppedCanvas.toDataURL('image/png')
1055
+ }
1056
+ });
1057
+ this.completeProcess(false);
1058
+ }
1059
+ // Return canvases to pool after use
1060
+ this.returnCanvasToPool(captureCanvas);
1061
+ this.returnCanvasToPool(croppedCanvas);
1062
+ }
871
1063
  async takeScreenshot() {
872
1064
  if (!this.videoRef || !this.lastDetectedBox)
873
1065
  return;
@@ -1301,6 +1493,26 @@ export class JaakStamps {
1301
1493
  "reflect": false,
1302
1494
  "defaultValue": "false"
1303
1495
  },
1496
+ "useDocumentDetector": {
1497
+ "type": "boolean",
1498
+ "attribute": "use-document-detector",
1499
+ "mutable": false,
1500
+ "complexType": {
1501
+ "original": "boolean",
1502
+ "resolved": "boolean",
1503
+ "references": {}
1504
+ },
1505
+ "required": false,
1506
+ "optional": false,
1507
+ "docs": {
1508
+ "tags": [],
1509
+ "text": ""
1510
+ },
1511
+ "getter": false,
1512
+ "setter": false,
1513
+ "reflect": false,
1514
+ "defaultValue": "true"
1515
+ },
1304
1516
  "preferredCamera": {
1305
1517
  "type": "string",
1306
1518
  "attribute": "preferred-camera",
@@ -1395,7 +1607,9 @@ export class JaakStamps {
1395
1607
  "cameraInfoWithAutofocus": {},
1396
1608
  "currentStatus": {},
1397
1609
  "performanceData": {},
1398
- "backDocumentTimerRemaining": {}
1610
+ "backDocumentTimerRemaining": {},
1611
+ "showManualCaptureButton": {},
1612
+ "captureStateVersion": {}
1399
1613
  };
1400
1614
  }
1401
1615
  static get events() {