metag-verify 0.1.0 → 0.1.1

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.
@@ -145,7 +145,7 @@ async function isVirtualCamera(stream) {
145
145
  }
146
146
 
147
147
  /** Versión que se envía en el header sdk-version (el backend no la valida). */
148
- const METAG_VERIFY_VERSION = '0.1.0';
148
+ const METAG_VERIFY_VERSION = '0.1.1';
149
149
 
150
150
  // ARCHIVO GENERADO por scripts/brand.mjs (npm run brand). No editar a mano.
151
151
  // Recursos de marca incrustados para que el SDK no dependa de archivos externos.
@@ -277,6 +277,126 @@ class BrightnessBoost {
277
277
  }
278
278
  }
279
279
 
280
+ // Apertura de la cámara. La trasera se elige con la misma regla que el SDK actual (captura de DPI):
281
+ // 1) el lente "camera2 0" (el principal en Android, con enfoque automático), 2) la trasera con el número
282
+ // más bajo ("camera 0, facing back"), 3) cualquiera "back/rear", 4) la que entregue facingMode "environment".
283
+ const VIDEO_SIZE = { width: { ideal: 1920 }, height: { ideal: 1080 } };
284
+ const REAR_LABEL = /back|rear|environment/i;
285
+ const MAIN_REAR_LABEL = /camera2\s?0/i;
286
+ const CAMERA_INDEX = /camera2?\s*(\d+)/i;
287
+ async function openCamera(facing) {
288
+ if (facing === 'user') {
289
+ return getStream({ ...VIDEO_SIZE, facingMode: 'user' });
290
+ }
291
+ // Si ya hay permiso, los nombres están disponibles y se abre directo el lente principal.
292
+ const known = await preferredRearCamera();
293
+ let stream = await getStream(known ? { ...VIDEO_SIZE, deviceId: { exact: known } } : { ...VIDEO_SIZE, facingMode: { ideal: 'environment' } });
294
+ // Primera vez: los nombres aparecen después del permiso; si hay un lente mejor, se cambia a ese.
295
+ if (!known) {
296
+ const preferred = await preferredRearCamera();
297
+ const current = stream.getVideoTracks()[0]?.getSettings().deviceId;
298
+ if (preferred && preferred !== current) {
299
+ stopStream(stream);
300
+ stream = await getStream({ ...VIDEO_SIZE, deviceId: { exact: preferred } });
301
+ }
302
+ }
303
+ return stream;
304
+ }
305
+ /** Indica si el dispositivo tiene más de una cámara (para mostrar el botón de cambiar). */
306
+ async function hasMultipleCameras() {
307
+ try {
308
+ const devices = await navigator.mediaDevices.enumerateDevices();
309
+ return devices.filter(d => d.kind === 'videoinput').length > 1;
310
+ }
311
+ catch {
312
+ return false;
313
+ }
314
+ }
315
+ /** Android reporta los modos de enfoque unos instantes después de abrir la cámara: se reintenta. */
316
+ const CAPABILITY_ATTEMPTS = 6;
317
+ const CAPABILITY_RETRY_MS = 250;
318
+ async function getCameraControls(stream) {
319
+ const caps = await readCapabilities(stream);
320
+ // Enfoque automático continuo cuando el lente lo soporta.
321
+ if (caps?.focusMode?.includes('continuous'))
322
+ await applyAdvanced(stream, { focusMode: 'continuous' });
323
+ return { tapFocus: caps?.focusMode?.includes('single-shot') === true };
324
+ }
325
+ /** Enfoca en un punto del cuadro; x e y van de 0 a 1 sobre la imagen real del lente. */
326
+ function focusAt(stream, x, y) {
327
+ return applyAdvanced(stream, { focusMode: 'single-shot', pointsOfInterest: [{ x, y }] });
328
+ }
329
+ /** Vuelve al enfoque automático continuo después de enfocar al tocar. */
330
+ function resumeAutofocus(stream) {
331
+ return applyAdvanced(stream, { focusMode: 'continuous' });
332
+ }
333
+ async function applyAdvanced(stream, constraint) {
334
+ const track = stream?.getVideoTracks()[0];
335
+ if (!track)
336
+ return false;
337
+ try {
338
+ await track.applyConstraints({ advanced: [constraint] });
339
+ return true;
340
+ }
341
+ catch {
342
+ return false;
343
+ }
344
+ }
345
+ function stopStream(stream) {
346
+ stream?.getTracks().forEach(track => track.stop());
347
+ }
348
+ function getStream(video) {
349
+ return navigator.mediaDevices.getUserMedia({ video, audio: false });
350
+ }
351
+ /**
352
+ * Lee las capacidades del lente esperando a que Android las reporte. ImageCapture obliga a Chrome
353
+ * a cargarlas; luego se reintenta unas veces hasta que aparezcan los modos de enfoque.
354
+ */
355
+ async function readCapabilities(stream) {
356
+ const track = stream?.getVideoTracks()[0];
357
+ if (!track?.getCapabilities)
358
+ return undefined;
359
+ const ImageCaptureCtor = globalThis.ImageCapture;
360
+ if (ImageCaptureCtor) {
361
+ try {
362
+ await new ImageCaptureCtor(track).getPhotoCapabilities();
363
+ }
364
+ catch {
365
+ // Sin soporte: se sigue con los reintentos.
366
+ }
367
+ }
368
+ let caps = track.getCapabilities();
369
+ for (let i = 1; i < CAPABILITY_ATTEMPTS && !caps.focusMode?.length; i++) {
370
+ await new Promise(resolve => setTimeout(resolve, CAPABILITY_RETRY_MS));
371
+ if (track.readyState !== 'live')
372
+ return undefined;
373
+ caps = track.getCapabilities();
374
+ }
375
+ return caps;
376
+ }
377
+ /**
378
+ * Android numera las cámaras ("camera 0, facing back", "camera2 2, facing back"...) y la principal,
379
+ * la que tiene flash y enfoque automático, es la trasera con el número más bajo.
380
+ */
381
+ function lowestIndexed(cams) {
382
+ const indexed = cams
383
+ .map(d => ({ d, n: Number(CAMERA_INDEX.exec(d.label)?.[1]) }))
384
+ .filter(c => Number.isFinite(c.n));
385
+ indexed.sort((a, b) => a.n - b.n);
386
+ return indexed[0]?.d;
387
+ }
388
+ async function preferredRearCamera() {
389
+ try {
390
+ const cams = (await navigator.mediaDevices.enumerateDevices()).filter(d => d.kind === 'videoinput' && d.label);
391
+ const rear = cams.filter(d => REAR_LABEL.test(d.label));
392
+ const chosen = rear.find(d => MAIN_REAR_LABEL.test(d.label)) ?? lowestIndexed(rear) ?? rear.find(d => /back/i.test(d.label)) ?? rear[0];
393
+ return chosen?.deviceId || null;
394
+ }
395
+ catch {
396
+ return null;
397
+ }
398
+ }
399
+
280
400
  const JPEG_QUALITY = 0.75;
281
401
  /** Cuenta regresiva antes de tomar la foto: 3, 2, 1. */
282
402
  const COUNTDOWN_FROM = 3;
@@ -294,6 +414,9 @@ const TIPS = [
294
414
  const EMPTY_POSTER = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
295
415
  /** Tiempo que se muestra "Verificación completada" antes de cerrar. */
296
416
  const SUCCESS_HOLD_MS = 1200;
417
+ /** Tiempo que se muestra el círculo de enfoque y tras el cual vuelve el enfoque continuo. */
418
+ const FOCUS_RING_MS = 800;
419
+ const FOCUS_HOLD_MS = 3000;
297
420
  const CAMERA_ERROR_MESSAGE = 'No pudimos acceder a la cámara. Revisa los permisos e intenta de nuevo.';
298
421
  /**
299
422
  * Foto Selfie, diseño A: cámara al 100% de la pantalla y botón rectangular "Tomar Fotografía".
@@ -310,6 +433,12 @@ class SelfieComponent {
310
433
  this.emptyPoster = EMPTY_POSTER;
311
434
  /** La cámara ya está transmitiendo imagen. */
312
435
  this.cameraReady = signal(false);
436
+ /** Por defecto la trasera; la frontal queda disponible con el botón de cambiar cámara. */
437
+ this.facing = signal('environment');
438
+ this.canSwitchCamera = signal(false);
439
+ this.tapFocus = false;
440
+ /** Punto donde se tocó para enfocar (px dentro de la pantalla); el id reinicia la animación. */
441
+ this.focusPoint = signal(null);
313
442
  this.tips = TIPS;
314
443
  this.tipIndex = signal(0);
315
444
  this.tipLeaving = signal(false);
@@ -323,6 +452,8 @@ class SelfieComponent {
323
452
  this.tipTimer = null;
324
453
  this.tipLeaveTimer = null;
325
454
  this.successTimer = null;
455
+ this.focusRingTimer = null;
456
+ this.focusHoldTimer = null;
326
457
  this.destroyed = false;
327
458
  }
328
459
  async ngAfterViewInit() {
@@ -334,6 +465,7 @@ class SelfieComponent {
334
465
  this.stopTips();
335
466
  if (this.successTimer)
336
467
  clearTimeout(this.successTimer);
468
+ this.clearFocusTimers();
337
469
  this.stopCountdown();
338
470
  this.stopCamera();
339
471
  }
@@ -352,6 +484,29 @@ class SelfieComponent {
352
484
  void this.captureAndSend();
353
485
  }, 1000);
354
486
  }
487
+ switchCamera() {
488
+ if (this.sending() || this.success() || this.countdown() !== null || !this.cameraReady())
489
+ return;
490
+ this.facing.update(f => (f === 'environment' ? 'user' : 'environment'));
491
+ this.stopCamera();
492
+ void this.startCamera();
493
+ }
494
+ /** Enfocar al tocar: se ignoran los toques sobre botones y textos. */
495
+ async onTap(event) {
496
+ if (!this.tapFocus || !this.cameraReady() || this.sending() || this.success())
497
+ return;
498
+ if (event.target.closest('button, .mv-hint'))
499
+ return;
500
+ const video = this.videoRef.nativeElement;
501
+ const rect = video.getBoundingClientRect();
502
+ const point = this.toVideoPoint(video, rect, event.clientX, event.clientY);
503
+ if (!point || !(await focusAt(this.stream, point.x, point.y)))
504
+ return;
505
+ this.clearFocusTimers();
506
+ this.focusPoint.set({ x: event.clientX - rect.left, y: event.clientY - rect.top, id: Date.now() });
507
+ this.focusRingTimer = setTimeout(() => this.focusPoint.set(null), FOCUS_RING_MS);
508
+ this.focusHoldTimer = setTimeout(() => void resumeAutofocus(this.stream), FOCUS_HOLD_MS);
509
+ }
355
510
  close() {
356
511
  if (this.sending() || this.success())
357
512
  return;
@@ -371,11 +526,9 @@ class SelfieComponent {
371
526
  void this.videoRef.nativeElement.play().catch(() => undefined);
372
527
  }
373
528
  async startCamera() {
529
+ this.cameraReady.set(false);
374
530
  try {
375
- this.stream = await navigator.mediaDevices.getUserMedia({
376
- video: { width: { ideal: 1920 }, height: { ideal: 1080 }, facingMode: 'user' },
377
- audio: false,
378
- });
531
+ this.stream = await openCamera(this.facing());
379
532
  }
380
533
  catch {
381
534
  this.dialog.set({ message: CAMERA_ERROR_MESSAGE, fatal: true });
@@ -392,8 +545,43 @@ class SelfieComponent {
392
545
  }
393
546
  const video = this.videoRef.nativeElement;
394
547
  video.srcObject = this.stream;
395
- void this.brightness.raise();
548
+ // El brillo al máximo solo sirve con la frontal: la pantalla ilumina el rostro.
549
+ if (this.facing() === 'user')
550
+ void this.brightness.raise();
551
+ this.canSwitchCamera.set(await hasMultipleCameras());
396
552
  await video.play().catch(() => undefined);
553
+ // Las capacidades se leen con el video ya transmitiendo: algunos Android no reportan la linterna antes.
554
+ const stream = this.stream;
555
+ const controls = await getCameraControls(stream);
556
+ // Si mientras tanto se cambió o cerró la cámara, estos controles ya no aplican.
557
+ if (this.destroyed || stream !== this.stream)
558
+ return;
559
+ this.tapFocus = controls.tapFocus;
560
+ }
561
+ /**
562
+ * Convierte el toque en pantalla a coordenadas 0-1 de la imagen real del lente,
563
+ * considerando el recorte de object-fit: cover y el espejo de la frontal.
564
+ */
565
+ toVideoPoint(video, rect, clientX, clientY) {
566
+ if (!video.videoWidth || !video.videoHeight)
567
+ return null;
568
+ const scale = Math.max(rect.width / video.videoWidth, rect.height / video.videoHeight);
569
+ const shownW = video.videoWidth * scale;
570
+ const shownH = video.videoHeight * scale;
571
+ let x = (clientX - rect.left - (rect.width - shownW) / 2) / shownW;
572
+ const y = (clientY - rect.top - (rect.height - shownH) / 2) / shownH;
573
+ if (this.facing() === 'user')
574
+ x = 1 - x;
575
+ const clamp = (v) => Math.min(1, Math.max(0, v));
576
+ return { x: clamp(x), y: clamp(y) };
577
+ }
578
+ clearFocusTimers() {
579
+ if (this.focusRingTimer)
580
+ clearTimeout(this.focusRingTimer);
581
+ if (this.focusHoldTimer)
582
+ clearTimeout(this.focusHoldTimer);
583
+ this.focusRingTimer = null;
584
+ this.focusHoldTimer = null;
397
585
  }
398
586
  async captureAndSend() {
399
587
  const video = this.videoRef.nativeElement;
@@ -476,16 +664,19 @@ class SelfieComponent {
476
664
  this.countdown.set(null);
477
665
  }
478
666
  stopCamera() {
479
- this.stream?.getTracks().forEach(track => track.stop());
667
+ stopStream(this.stream);
480
668
  this.stream = null;
669
+ this.tapFocus = false;
670
+ this.clearFocusTimers();
671
+ this.focusPoint.set(null);
481
672
  void this.brightness.restore();
482
673
  }
483
674
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SelfieComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
484
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SelfieComponent, isStandalone: true, selector: "mv-selfie", inputs: { submit: "submit" }, outputs: { completed: "completed", closed: "closed", blocked: "blocked", failed: "failed" }, viewQueries: [{ propertyName: "videoRef", first: true, predicate: ["video"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"mv-camera\">\n <!-- poster transparente: evita el \u00EDcono gris de \"play\" que Android WebView muestra mientras arranca la c\u00E1mara -->\n <video #video class=\"mv-video\" [class.mv-video--ready]=\"cameraReady()\" [poster]=\"emptyPoster\"\n muted autoplay playsinline (playing)=\"cameraReady.set(true)\"></video>\n <div class=\"mv-scrim\" aria-hidden=\"true\"></div>\n\n <div class=\"mv-ui\">\n <header class=\"mv-head\">\n <div class=\"mv-title-group\">\n <img class=\"mv-title-logo\" [src]=\"gLogo\" alt=\"\" aria-hidden=\"true\" />\n <h2 class=\"mv-title\">Foto Selfie</h2>\n </div>\n <button type=\"button\" class=\"mv-close\" aria-label=\"Cerrar\" [disabled]=\"sending() || success()\" (click)=\"close()\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M6 6l12 12M18 6L6 18\" /></svg>\n </button>\n </header>\n\n <div class=\"mv-spacer\"></div>\n\n <footer class=\"mv-foot\">\n @if (countdown() !== null) {\n <!-- @for recrea el elemento en cada n\u00FAmero para repetir la animaci\u00F3n -->\n @for (n of [countdown()]; track n) {\n <p class=\"mv-hint mv-countdown\" aria-live=\"assertive\">{{ n }}</p>\n }\n } @else if (success()) {\n <p class=\"mv-hint mv-success-text\" role=\"status\" aria-live=\"polite\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M5 12.5l4.5 4.5L19 7.5\" /></svg>\n Verificaci\u00F3n completada\n </p>\n } @else {\n @if (sending()) {\n <p class=\"mv-hint\" aria-live=\"polite\">Validando foto...</p>\n } @else {\n <!-- Tips rotativos: el recuadro se queda fijo y solo cambia el contenido -->\n <div class=\"mv-hint mv-tips\">\n <span class=\"mv-sr-only\">{{ tips[0].text }}</span>\n @for (i of [tipIndex()]; track i) {\n <span class=\"mv-tip\" [class.mv-tip--leaving]=\"tipLeaving()\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 24 24\"><path [attr.d]=\"tips[i].icon\" /></svg>\n {{ tips[i].text }}\n </span>\n }\n </div>\n }\n }\n @if (sending()) {\n <span class=\"mv-sending-bar\" aria-hidden=\"true\"></span>\n } @else if (success()) {\n <span class=\"mv-success-bar\" aria-hidden=\"true\"></span>\n }\n <!-- Oculto mientras se env\u00EDa y al confirmar el \u00E9xito; vuelve a aparecer si hay error para reintentar -->\n @if (!sending() && !success()) {\n <button type=\"button\" class=\"mv-button\" [disabled]=\"!cameraReady() || countdown() !== null\" (click)=\"takePhoto()\">\n Tomar Fotograf\u00EDa\n </button>\n }\n </footer>\n </div>\n\n @if (dialog(); as d) {\n <mv-dialog [message]=\"d.message\" (closed)=\"onDialogClosed()\"></mv-dialog>\n }\n</div>\n", styles: [":host{display:block;height:100%}.mv-camera{position:relative;height:100%;overflow:hidden;background:#000}.mv-video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transform:scaleX(-1);background:#000;opacity:0;transition:opacity .3s ease}.mv-video--ready{opacity:1}.mv-scrim{position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,#00000080,#0000 22% 62%,#0009)}.mv-ui{position:absolute;inset:0;display:flex;flex-direction:column;padding:max(22px,env(safe-area-inset-top)) 22px max(22px,env(safe-area-inset-bottom));color:#fff}.mv-head{display:flex;align-items:center;justify-content:space-between;gap:16px}.mv-title-group{display:flex;align-items:center;gap:10px;min-width:0}.mv-title-logo{flex-shrink:0;width:36px;height:36px}.mv-title{margin:0;font-family:var(--mv-font);font-size:20px;font-weight:600;letter-spacing:-.025em;line-height:1.2;font-size:26px;line-height:36px;color:#fff}.mv-close{flex-shrink:0;width:36px;height:36px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:#ffffff2e;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer}.mv-close svg{width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round}.mv-close:disabled{opacity:.4;cursor:default}.mv-close:focus-visible{outline:2px solid #ffffff;outline-offset:2px}.mv-spacer{flex:1}.mv-foot{display:flex;flex-direction:column;gap:16px}.mv-hint{margin:0 0 16px;font-family:var(--mv-font);font-size:14px;font-weight:400;line-height:1.5;align-self:center;max-width:100%;padding:10px 18px;border-radius:14px;background:#09090b80;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);font-size:18px;font-weight:500;color:#fff;text-align:center}.mv-tips{width:min(100%,300px);overflow:hidden}.mv-tip{display:inline-flex;align-items:center;justify-content:center;gap:8px;animation:mv-tip-in .3s ease-out backwards;transition:opacity .25s ease,transform .25s ease}.mv-tip svg{width:20px;height:20px;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.mv-tip--leaving{opacity:0;transform:translateY(-6px)}@keyframes mv-tip-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}@media (prefers-reduced-motion: reduce){.mv-tip{animation:none;transition:none}}.mv-sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.mv-countdown{min-width:64px;font-size:28px;font-weight:600;line-height:1;font-variant-numeric:tabular-nums;color:#fff;animation:mv-count-pop .35s ease-out both}@keyframes mv-count-pop{0%{opacity:0;transform:scale(.7)}to{opacity:1;transform:scale(1)}}@media (prefers-reduced-motion: reduce){.mv-countdown{animation:none}}.mv-button{height:52px;border:0;border-radius:14px;font-family:var(--mv-font);font-size:15px;font-weight:600;letter-spacing:-.01em;width:100%;background:var(--mv-primary);color:#fff;cursor:pointer;transition:background-color .15s ease,transform .12s ease,opacity .15s ease;font-size:18px}.mv-button:hover{background:var(--mv-primary-hover)}.mv-button:active{transform:scale(.985)}.mv-button:disabled{opacity:.5;cursor:default;transform:none}.mv-button:focus-visible{outline:2px solid var(--mv-primary);outline-offset:3px}.mv-sending-bar{position:relative;height:8px;border-radius:999px;background:#ffffff4d;overflow:hidden}.mv-sending-bar:after{content:\"\";position:absolute;top:0;bottom:0;width:40%;border-radius:999px;background:var(--mv-accent);animation:mv-track-slide 1.1s cubic-bezier(.6,.1,.4,.9) infinite}@keyframes mv-track-slide{0%{left:-40%}to{left:100%}}@media (prefers-reduced-motion: reduce){.mv-sending-bar:after{animation-duration:2.4s}}.mv-success-bar{height:8px;border-radius:999px;background:#ffffff4d;overflow:hidden}.mv-success-bar:after{content:\"\";display:block;height:100%;border-radius:999px;background:var(--mv-success);animation:mv-success-fill .35s ease-out both}@keyframes mv-success-fill{0%{width:40%}to{width:100%}}.mv-success-text{display:inline-flex;align-items:center;justify-content:center;gap:8px;color:#fff;animation:mv-count-pop .3s ease-out both}.mv-success-text svg{width:22px;height:22px;flex-shrink:0;stroke:var(--mv-success);fill:none;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}@media (prefers-reduced-motion: reduce){.mv-success-bar:after,.mv-success-text{animation:none}}@media (min-width: 768px){.mv-ui{padding:28px 32px}.mv-foot{width:100%;max-width:380px;margin:0 auto}}\n"], dependencies: [{ kind: "component", type: DialogComponent, selector: "mv-dialog", inputs: ["title", "message", "buttonLabel"], outputs: ["closed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
675
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SelfieComponent, isStandalone: true, selector: "mv-selfie", inputs: { submit: "submit" }, outputs: { completed: "completed", closed: "closed", blocked: "blocked", failed: "failed" }, viewQueries: [{ propertyName: "videoRef", first: true, predicate: ["video"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"mv-camera\">\n <!-- poster transparente: evita el \u00EDcono gris de \"play\" que Android WebView muestra mientras arranca la c\u00E1mara -->\n <video #video class=\"mv-video\" [class.mv-video--ready]=\"cameraReady()\" [class.mv-video--mirror]=\"facing() === 'user'\" [poster]=\"emptyPoster\"\n muted autoplay playsinline (playing)=\"cameraReady.set(true)\"></video>\n <div class=\"mv-scrim\" aria-hidden=\"true\"></div>\n\n @if (focusPoint(); as p) {\n @for (id of [p.id]; track id) {\n <span class=\"mv-focus-ring\" [style.left.px]=\"p.x\" [style.top.px]=\"p.y\" aria-hidden=\"true\"></span>\n }\n }\n\n <!-- Tocar la imagen enfoca en ese punto (si el lente lo soporta) -->\n <div class=\"mv-ui\" (click)=\"onTap($event)\">\n <header class=\"mv-head\">\n <div class=\"mv-title-group\">\n <img class=\"mv-title-logo\" [src]=\"gLogo\" alt=\"\" aria-hidden=\"true\" />\n <h2 class=\"mv-title\">Foto Selfie</h2>\n </div>\n <!-- La X arriba y debajo los controles en columna, como en las apps de c\u00E1mara -->\n <div class=\"mv-actions\">\n <button type=\"button\" class=\"mv-close\" aria-label=\"Cerrar\" [disabled]=\"sending() || success()\" (click)=\"close()\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M6 6l12 12M18 6L6 18\" /></svg>\n </button>\n @if (!sending() && !success()) {\n <div class=\"mv-tools\">\n @if (canSwitchCamera()) {\n <button type=\"button\" class=\"mv-close mv-tool\" aria-label=\"Cambiar c\u00E1mara\"\n [disabled]=\"!cameraReady() || countdown() !== null\" (click)=\"switchCamera()\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M11 19H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5 M13 5h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-5 M12 9a3 3 0 1 0 0 6a3 3 0 1 0 0-6 M18 22l-3-3l3-3 M6 2l3 3l-3 3\" /></svg>\n </button>\n }\n </div>\n }\n </div>\n </header>\n\n <div class=\"mv-spacer\"></div>\n\n <footer class=\"mv-foot\">\n @if (countdown() !== null) {\n <!-- @for recrea el elemento en cada n\u00FAmero para repetir la animaci\u00F3n -->\n @for (n of [countdown()]; track n) {\n <p class=\"mv-hint mv-countdown\" aria-live=\"assertive\">{{ n }}</p>\n }\n } @else if (success()) {\n <p class=\"mv-hint mv-success-text\" role=\"status\" aria-live=\"polite\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M5 12.5l4.5 4.5L19 7.5\" /></svg>\n Verificaci\u00F3n completada\n </p>\n } @else {\n @if (sending()) {\n <p class=\"mv-hint\" aria-live=\"polite\">Validando foto...</p>\n } @else {\n <!-- Tips rotativos: el recuadro se queda fijo y solo cambia el contenido -->\n <div class=\"mv-hint mv-tips\">\n <span class=\"mv-sr-only\">{{ tips[0].text }}</span>\n @for (i of [tipIndex()]; track i) {\n <span class=\"mv-tip\" [class.mv-tip--leaving]=\"tipLeaving()\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 24 24\"><path [attr.d]=\"tips[i].icon\" /></svg>\n {{ tips[i].text }}\n </span>\n }\n </div>\n }\n }\n @if (sending()) {\n <span class=\"mv-sending-bar\" aria-hidden=\"true\"></span>\n } @else if (success()) {\n <span class=\"mv-success-bar\" aria-hidden=\"true\"></span>\n }\n <!-- Oculto mientras se env\u00EDa y al confirmar el \u00E9xito; vuelve a aparecer si hay error para reintentar -->\n @if (!sending() && !success()) {\n <button type=\"button\" class=\"mv-button\" [disabled]=\"!cameraReady() || countdown() !== null\" (click)=\"takePhoto()\">\n Tomar Fotograf\u00EDa\n </button>\n }\n </footer>\n </div>\n\n @if (dialog(); as d) {\n <mv-dialog [message]=\"d.message\" (closed)=\"onDialogClosed()\"></mv-dialog>\n }\n</div>\n", styles: [":host{display:block;height:100%}.mv-camera{position:relative;height:100%;overflow:hidden;background:#000}.mv-video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#000;opacity:0;transition:opacity .3s ease}.mv-video--mirror{transform:scaleX(-1)}.mv-video--ready{opacity:1}.mv-scrim{position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,#00000080,#0000 22% 62%,#0009)}.mv-focus-ring{position:absolute;width:64px;height:64px;margin:-32px 0 0 -32px;border-radius:50%;box-shadow:inset 0 0 0 1.5px #fff;pointer-events:none;animation:mv-focus .8s ease-out both}@keyframes mv-focus{0%{opacity:0;transform:scale(1.4)}25%{opacity:1;transform:scale(1)}75%{opacity:1}to{opacity:0;transform:scale(1)}}@media (prefers-reduced-motion: reduce){.mv-focus-ring{animation:none}}.mv-ui{position:absolute;inset:0;display:flex;flex-direction:column;padding:max(22px,env(safe-area-inset-top)) 22px max(22px,env(safe-area-inset-bottom));color:#fff}.mv-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.mv-title-group{display:flex;align-items:center;gap:10px;min-width:0}.mv-title-logo{flex-shrink:0;width:36px;height:36px}.mv-title{margin:0;font-family:var(--mv-font);font-size:20px;font-weight:600;letter-spacing:-.025em;line-height:1.2;font-size:26px;line-height:36px;color:#fff}.mv-close{flex-shrink:0;width:36px;height:36px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:#ffffff2e;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer}.mv-close svg{width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round}.mv-close:disabled{opacity:.4;cursor:default}.mv-close:focus-visible{outline:2px solid #ffffff;outline-offset:2px}.mv-actions{display:flex;flex-direction:column;align-items:center;gap:16px}.mv-tools{display:flex;flex-direction:column;gap:12px}.mv-tool svg{width:20px;height:20px;stroke-width:2;stroke-linejoin:round}.mv-spacer{flex:1}.mv-foot{display:flex;flex-direction:column;gap:16px}.mv-hint{margin:0 0 16px;font-family:var(--mv-font);font-size:14px;font-weight:400;line-height:1.5;align-self:center;max-width:100%;padding:10px 18px;border-radius:14px;background:#09090b80;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);font-size:18px;font-weight:500;color:#fff;text-align:center}.mv-tips{width:min(100%,300px);overflow:hidden}.mv-tip{display:inline-flex;align-items:center;justify-content:center;gap:8px;animation:mv-tip-in .3s ease-out backwards;transition:opacity .25s ease,transform .25s ease}.mv-tip svg{width:20px;height:20px;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.mv-tip--leaving{opacity:0;transform:translateY(-6px)}@keyframes mv-tip-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}@media (prefers-reduced-motion: reduce){.mv-tip{animation:none;transition:none}}.mv-sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.mv-countdown{min-width:64px;font-size:28px;font-weight:600;line-height:1;font-variant-numeric:tabular-nums;color:#fff;animation:mv-count-pop .35s ease-out both}@keyframes mv-count-pop{0%{opacity:0;transform:scale(.7)}to{opacity:1;transform:scale(1)}}@media (prefers-reduced-motion: reduce){.mv-countdown{animation:none}}.mv-button{height:52px;border:0;border-radius:14px;font-family:var(--mv-font);font-size:15px;font-weight:600;letter-spacing:-.01em;width:100%;background:var(--mv-primary);color:#fff;cursor:pointer;transition:background-color .15s ease,transform .12s ease,opacity .15s ease;font-size:18px}.mv-button:hover{background:var(--mv-primary-hover)}.mv-button:active{transform:scale(.985)}.mv-button:disabled{opacity:.5;cursor:default;transform:none}.mv-button:focus-visible{outline:2px solid var(--mv-primary);outline-offset:3px}.mv-sending-bar{position:relative;height:8px;border-radius:999px;background:#ffffff4d;overflow:hidden}.mv-sending-bar:after{content:\"\";position:absolute;top:0;bottom:0;width:40%;border-radius:999px;background:var(--mv-accent);animation:mv-track-slide 1.1s cubic-bezier(.6,.1,.4,.9) infinite}@keyframes mv-track-slide{0%{left:-40%}to{left:100%}}@media (prefers-reduced-motion: reduce){.mv-sending-bar:after{animation-duration:2.4s}}.mv-success-bar{height:8px;border-radius:999px;background:#ffffff4d;overflow:hidden}.mv-success-bar:after{content:\"\";display:block;height:100%;border-radius:999px;background:var(--mv-success);animation:mv-success-fill .35s ease-out both}@keyframes mv-success-fill{0%{width:40%}to{width:100%}}.mv-success-text{display:inline-flex;align-items:center;justify-content:center;gap:8px;color:#fff;animation:mv-count-pop .3s ease-out both}.mv-success-text svg{width:22px;height:22px;flex-shrink:0;stroke:var(--mv-success);fill:none;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}@media (prefers-reduced-motion: reduce){.mv-success-bar:after,.mv-success-text{animation:none}}@media (min-width: 768px){.mv-ui{padding:28px 32px}.mv-foot{width:100%;max-width:380px;margin:0 auto}}\n"], dependencies: [{ kind: "component", type: DialogComponent, selector: "mv-dialog", inputs: ["title", "message", "buttonLabel"], outputs: ["closed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
485
676
  }
486
677
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SelfieComponent, decorators: [{
487
678
  type: Component,
488
- args: [{ selector: 'mv-selfie', standalone: true, imports: [DialogComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"mv-camera\">\n <!-- poster transparente: evita el \u00EDcono gris de \"play\" que Android WebView muestra mientras arranca la c\u00E1mara -->\n <video #video class=\"mv-video\" [class.mv-video--ready]=\"cameraReady()\" [poster]=\"emptyPoster\"\n muted autoplay playsinline (playing)=\"cameraReady.set(true)\"></video>\n <div class=\"mv-scrim\" aria-hidden=\"true\"></div>\n\n <div class=\"mv-ui\">\n <header class=\"mv-head\">\n <div class=\"mv-title-group\">\n <img class=\"mv-title-logo\" [src]=\"gLogo\" alt=\"\" aria-hidden=\"true\" />\n <h2 class=\"mv-title\">Foto Selfie</h2>\n </div>\n <button type=\"button\" class=\"mv-close\" aria-label=\"Cerrar\" [disabled]=\"sending() || success()\" (click)=\"close()\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M6 6l12 12M18 6L6 18\" /></svg>\n </button>\n </header>\n\n <div class=\"mv-spacer\"></div>\n\n <footer class=\"mv-foot\">\n @if (countdown() !== null) {\n <!-- @for recrea el elemento en cada n\u00FAmero para repetir la animaci\u00F3n -->\n @for (n of [countdown()]; track n) {\n <p class=\"mv-hint mv-countdown\" aria-live=\"assertive\">{{ n }}</p>\n }\n } @else if (success()) {\n <p class=\"mv-hint mv-success-text\" role=\"status\" aria-live=\"polite\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M5 12.5l4.5 4.5L19 7.5\" /></svg>\n Verificaci\u00F3n completada\n </p>\n } @else {\n @if (sending()) {\n <p class=\"mv-hint\" aria-live=\"polite\">Validando foto...</p>\n } @else {\n <!-- Tips rotativos: el recuadro se queda fijo y solo cambia el contenido -->\n <div class=\"mv-hint mv-tips\">\n <span class=\"mv-sr-only\">{{ tips[0].text }}</span>\n @for (i of [tipIndex()]; track i) {\n <span class=\"mv-tip\" [class.mv-tip--leaving]=\"tipLeaving()\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 24 24\"><path [attr.d]=\"tips[i].icon\" /></svg>\n {{ tips[i].text }}\n </span>\n }\n </div>\n }\n }\n @if (sending()) {\n <span class=\"mv-sending-bar\" aria-hidden=\"true\"></span>\n } @else if (success()) {\n <span class=\"mv-success-bar\" aria-hidden=\"true\"></span>\n }\n <!-- Oculto mientras se env\u00EDa y al confirmar el \u00E9xito; vuelve a aparecer si hay error para reintentar -->\n @if (!sending() && !success()) {\n <button type=\"button\" class=\"mv-button\" [disabled]=\"!cameraReady() || countdown() !== null\" (click)=\"takePhoto()\">\n Tomar Fotograf\u00EDa\n </button>\n }\n </footer>\n </div>\n\n @if (dialog(); as d) {\n <mv-dialog [message]=\"d.message\" (closed)=\"onDialogClosed()\"></mv-dialog>\n }\n</div>\n", styles: [":host{display:block;height:100%}.mv-camera{position:relative;height:100%;overflow:hidden;background:#000}.mv-video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transform:scaleX(-1);background:#000;opacity:0;transition:opacity .3s ease}.mv-video--ready{opacity:1}.mv-scrim{position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,#00000080,#0000 22% 62%,#0009)}.mv-ui{position:absolute;inset:0;display:flex;flex-direction:column;padding:max(22px,env(safe-area-inset-top)) 22px max(22px,env(safe-area-inset-bottom));color:#fff}.mv-head{display:flex;align-items:center;justify-content:space-between;gap:16px}.mv-title-group{display:flex;align-items:center;gap:10px;min-width:0}.mv-title-logo{flex-shrink:0;width:36px;height:36px}.mv-title{margin:0;font-family:var(--mv-font);font-size:20px;font-weight:600;letter-spacing:-.025em;line-height:1.2;font-size:26px;line-height:36px;color:#fff}.mv-close{flex-shrink:0;width:36px;height:36px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:#ffffff2e;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer}.mv-close svg{width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round}.mv-close:disabled{opacity:.4;cursor:default}.mv-close:focus-visible{outline:2px solid #ffffff;outline-offset:2px}.mv-spacer{flex:1}.mv-foot{display:flex;flex-direction:column;gap:16px}.mv-hint{margin:0 0 16px;font-family:var(--mv-font);font-size:14px;font-weight:400;line-height:1.5;align-self:center;max-width:100%;padding:10px 18px;border-radius:14px;background:#09090b80;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);font-size:18px;font-weight:500;color:#fff;text-align:center}.mv-tips{width:min(100%,300px);overflow:hidden}.mv-tip{display:inline-flex;align-items:center;justify-content:center;gap:8px;animation:mv-tip-in .3s ease-out backwards;transition:opacity .25s ease,transform .25s ease}.mv-tip svg{width:20px;height:20px;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.mv-tip--leaving{opacity:0;transform:translateY(-6px)}@keyframes mv-tip-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}@media (prefers-reduced-motion: reduce){.mv-tip{animation:none;transition:none}}.mv-sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.mv-countdown{min-width:64px;font-size:28px;font-weight:600;line-height:1;font-variant-numeric:tabular-nums;color:#fff;animation:mv-count-pop .35s ease-out both}@keyframes mv-count-pop{0%{opacity:0;transform:scale(.7)}to{opacity:1;transform:scale(1)}}@media (prefers-reduced-motion: reduce){.mv-countdown{animation:none}}.mv-button{height:52px;border:0;border-radius:14px;font-family:var(--mv-font);font-size:15px;font-weight:600;letter-spacing:-.01em;width:100%;background:var(--mv-primary);color:#fff;cursor:pointer;transition:background-color .15s ease,transform .12s ease,opacity .15s ease;font-size:18px}.mv-button:hover{background:var(--mv-primary-hover)}.mv-button:active{transform:scale(.985)}.mv-button:disabled{opacity:.5;cursor:default;transform:none}.mv-button:focus-visible{outline:2px solid var(--mv-primary);outline-offset:3px}.mv-sending-bar{position:relative;height:8px;border-radius:999px;background:#ffffff4d;overflow:hidden}.mv-sending-bar:after{content:\"\";position:absolute;top:0;bottom:0;width:40%;border-radius:999px;background:var(--mv-accent);animation:mv-track-slide 1.1s cubic-bezier(.6,.1,.4,.9) infinite}@keyframes mv-track-slide{0%{left:-40%}to{left:100%}}@media (prefers-reduced-motion: reduce){.mv-sending-bar:after{animation-duration:2.4s}}.mv-success-bar{height:8px;border-radius:999px;background:#ffffff4d;overflow:hidden}.mv-success-bar:after{content:\"\";display:block;height:100%;border-radius:999px;background:var(--mv-success);animation:mv-success-fill .35s ease-out both}@keyframes mv-success-fill{0%{width:40%}to{width:100%}}.mv-success-text{display:inline-flex;align-items:center;justify-content:center;gap:8px;color:#fff;animation:mv-count-pop .3s ease-out both}.mv-success-text svg{width:22px;height:22px;flex-shrink:0;stroke:var(--mv-success);fill:none;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}@media (prefers-reduced-motion: reduce){.mv-success-bar:after,.mv-success-text{animation:none}}@media (min-width: 768px){.mv-ui{padding:28px 32px}.mv-foot{width:100%;max-width:380px;margin:0 auto}}\n"] }]
679
+ args: [{ selector: 'mv-selfie', standalone: true, imports: [DialogComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"mv-camera\">\n <!-- poster transparente: evita el \u00EDcono gris de \"play\" que Android WebView muestra mientras arranca la c\u00E1mara -->\n <video #video class=\"mv-video\" [class.mv-video--ready]=\"cameraReady()\" [class.mv-video--mirror]=\"facing() === 'user'\" [poster]=\"emptyPoster\"\n muted autoplay playsinline (playing)=\"cameraReady.set(true)\"></video>\n <div class=\"mv-scrim\" aria-hidden=\"true\"></div>\n\n @if (focusPoint(); as p) {\n @for (id of [p.id]; track id) {\n <span class=\"mv-focus-ring\" [style.left.px]=\"p.x\" [style.top.px]=\"p.y\" aria-hidden=\"true\"></span>\n }\n }\n\n <!-- Tocar la imagen enfoca en ese punto (si el lente lo soporta) -->\n <div class=\"mv-ui\" (click)=\"onTap($event)\">\n <header class=\"mv-head\">\n <div class=\"mv-title-group\">\n <img class=\"mv-title-logo\" [src]=\"gLogo\" alt=\"\" aria-hidden=\"true\" />\n <h2 class=\"mv-title\">Foto Selfie</h2>\n </div>\n <!-- La X arriba y debajo los controles en columna, como en las apps de c\u00E1mara -->\n <div class=\"mv-actions\">\n <button type=\"button\" class=\"mv-close\" aria-label=\"Cerrar\" [disabled]=\"sending() || success()\" (click)=\"close()\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M6 6l12 12M18 6L6 18\" /></svg>\n </button>\n @if (!sending() && !success()) {\n <div class=\"mv-tools\">\n @if (canSwitchCamera()) {\n <button type=\"button\" class=\"mv-close mv-tool\" aria-label=\"Cambiar c\u00E1mara\"\n [disabled]=\"!cameraReady() || countdown() !== null\" (click)=\"switchCamera()\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M11 19H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5 M13 5h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-5 M12 9a3 3 0 1 0 0 6a3 3 0 1 0 0-6 M18 22l-3-3l3-3 M6 2l3 3l-3 3\" /></svg>\n </button>\n }\n </div>\n }\n </div>\n </header>\n\n <div class=\"mv-spacer\"></div>\n\n <footer class=\"mv-foot\">\n @if (countdown() !== null) {\n <!-- @for recrea el elemento en cada n\u00FAmero para repetir la animaci\u00F3n -->\n @for (n of [countdown()]; track n) {\n <p class=\"mv-hint mv-countdown\" aria-live=\"assertive\">{{ n }}</p>\n }\n } @else if (success()) {\n <p class=\"mv-hint mv-success-text\" role=\"status\" aria-live=\"polite\">\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M5 12.5l4.5 4.5L19 7.5\" /></svg>\n Verificaci\u00F3n completada\n </p>\n } @else {\n @if (sending()) {\n <p class=\"mv-hint\" aria-live=\"polite\">Validando foto...</p>\n } @else {\n <!-- Tips rotativos: el recuadro se queda fijo y solo cambia el contenido -->\n <div class=\"mv-hint mv-tips\">\n <span class=\"mv-sr-only\">{{ tips[0].text }}</span>\n @for (i of [tipIndex()]; track i) {\n <span class=\"mv-tip\" [class.mv-tip--leaving]=\"tipLeaving()\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 24 24\"><path [attr.d]=\"tips[i].icon\" /></svg>\n {{ tips[i].text }}\n </span>\n }\n </div>\n }\n }\n @if (sending()) {\n <span class=\"mv-sending-bar\" aria-hidden=\"true\"></span>\n } @else if (success()) {\n <span class=\"mv-success-bar\" aria-hidden=\"true\"></span>\n }\n <!-- Oculto mientras se env\u00EDa y al confirmar el \u00E9xito; vuelve a aparecer si hay error para reintentar -->\n @if (!sending() && !success()) {\n <button type=\"button\" class=\"mv-button\" [disabled]=\"!cameraReady() || countdown() !== null\" (click)=\"takePhoto()\">\n Tomar Fotograf\u00EDa\n </button>\n }\n </footer>\n </div>\n\n @if (dialog(); as d) {\n <mv-dialog [message]=\"d.message\" (closed)=\"onDialogClosed()\"></mv-dialog>\n }\n</div>\n", styles: [":host{display:block;height:100%}.mv-camera{position:relative;height:100%;overflow:hidden;background:#000}.mv-video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#000;opacity:0;transition:opacity .3s ease}.mv-video--mirror{transform:scaleX(-1)}.mv-video--ready{opacity:1}.mv-scrim{position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,#00000080,#0000 22% 62%,#0009)}.mv-focus-ring{position:absolute;width:64px;height:64px;margin:-32px 0 0 -32px;border-radius:50%;box-shadow:inset 0 0 0 1.5px #fff;pointer-events:none;animation:mv-focus .8s ease-out both}@keyframes mv-focus{0%{opacity:0;transform:scale(1.4)}25%{opacity:1;transform:scale(1)}75%{opacity:1}to{opacity:0;transform:scale(1)}}@media (prefers-reduced-motion: reduce){.mv-focus-ring{animation:none}}.mv-ui{position:absolute;inset:0;display:flex;flex-direction:column;padding:max(22px,env(safe-area-inset-top)) 22px max(22px,env(safe-area-inset-bottom));color:#fff}.mv-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.mv-title-group{display:flex;align-items:center;gap:10px;min-width:0}.mv-title-logo{flex-shrink:0;width:36px;height:36px}.mv-title{margin:0;font-family:var(--mv-font);font-size:20px;font-weight:600;letter-spacing:-.025em;line-height:1.2;font-size:26px;line-height:36px;color:#fff}.mv-close{flex-shrink:0;width:36px;height:36px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:#ffffff2e;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer}.mv-close svg{width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round}.mv-close:disabled{opacity:.4;cursor:default}.mv-close:focus-visible{outline:2px solid #ffffff;outline-offset:2px}.mv-actions{display:flex;flex-direction:column;align-items:center;gap:16px}.mv-tools{display:flex;flex-direction:column;gap:12px}.mv-tool svg{width:20px;height:20px;stroke-width:2;stroke-linejoin:round}.mv-spacer{flex:1}.mv-foot{display:flex;flex-direction:column;gap:16px}.mv-hint{margin:0 0 16px;font-family:var(--mv-font);font-size:14px;font-weight:400;line-height:1.5;align-self:center;max-width:100%;padding:10px 18px;border-radius:14px;background:#09090b80;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);font-size:18px;font-weight:500;color:#fff;text-align:center}.mv-tips{width:min(100%,300px);overflow:hidden}.mv-tip{display:inline-flex;align-items:center;justify-content:center;gap:8px;animation:mv-tip-in .3s ease-out backwards;transition:opacity .25s ease,transform .25s ease}.mv-tip svg{width:20px;height:20px;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.mv-tip--leaving{opacity:0;transform:translateY(-6px)}@keyframes mv-tip-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}@media (prefers-reduced-motion: reduce){.mv-tip{animation:none;transition:none}}.mv-sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.mv-countdown{min-width:64px;font-size:28px;font-weight:600;line-height:1;font-variant-numeric:tabular-nums;color:#fff;animation:mv-count-pop .35s ease-out both}@keyframes mv-count-pop{0%{opacity:0;transform:scale(.7)}to{opacity:1;transform:scale(1)}}@media (prefers-reduced-motion: reduce){.mv-countdown{animation:none}}.mv-button{height:52px;border:0;border-radius:14px;font-family:var(--mv-font);font-size:15px;font-weight:600;letter-spacing:-.01em;width:100%;background:var(--mv-primary);color:#fff;cursor:pointer;transition:background-color .15s ease,transform .12s ease,opacity .15s ease;font-size:18px}.mv-button:hover{background:var(--mv-primary-hover)}.mv-button:active{transform:scale(.985)}.mv-button:disabled{opacity:.5;cursor:default;transform:none}.mv-button:focus-visible{outline:2px solid var(--mv-primary);outline-offset:3px}.mv-sending-bar{position:relative;height:8px;border-radius:999px;background:#ffffff4d;overflow:hidden}.mv-sending-bar:after{content:\"\";position:absolute;top:0;bottom:0;width:40%;border-radius:999px;background:var(--mv-accent);animation:mv-track-slide 1.1s cubic-bezier(.6,.1,.4,.9) infinite}@keyframes mv-track-slide{0%{left:-40%}to{left:100%}}@media (prefers-reduced-motion: reduce){.mv-sending-bar:after{animation-duration:2.4s}}.mv-success-bar{height:8px;border-radius:999px;background:#ffffff4d;overflow:hidden}.mv-success-bar:after{content:\"\";display:block;height:100%;border-radius:999px;background:var(--mv-success);animation:mv-success-fill .35s ease-out both}@keyframes mv-success-fill{0%{width:40%}to{width:100%}}.mv-success-text{display:inline-flex;align-items:center;justify-content:center;gap:8px;color:#fff;animation:mv-count-pop .3s ease-out both}.mv-success-text svg{width:22px;height:22px;flex-shrink:0;stroke:var(--mv-success);fill:none;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}@media (prefers-reduced-motion: reduce){.mv-success-bar:after,.mv-success-text{animation:none}}@media (min-width: 768px){.mv-ui{padding:28px 32px}.mv-foot{width:100%;max-width:380px;margin:0 auto}}\n"] }]
489
680
  }], propDecorators: { submit: [{
490
681
  type: Input,
491
682
  args: [{ required: true }]