@simprints/simface-sdk 0.4.1 → 0.6.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.
@@ -0,0 +1,746 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { LitElement, html, css } from 'lit';
8
+ import { customElement, property, query, state } from 'lit/decorators.js';
9
+ import { blobToImage, captureFromCamera } from '../services/camera.js';
10
+ import { assessFaceQuality, assessFaceQualityForVideo } from '../services/face-detection.js';
11
+ import { AUTO_CAPTURE_ANALYSIS_INTERVAL_MS, AUTO_CAPTURE_COUNTDOWN_MS, CAPTURE_GUIDE_MASK_PATH, CAPTURE_GUIDE_PATH, autoCaptureCompleteMessage, autoCaptureCountdownMessage, } from '../shared/auto-capture.js';
12
+ /**
13
+ * <simface-capture> — Web Component for capturing and quality-checking face images.
14
+ *
15
+ * Emits:
16
+ * - simface-captured: { imageBlob: Blob } when a quality-checked image is confirmed
17
+ * - simface-cancelled: when the user cancels
18
+ * - simface-error: { error: string } on errors
19
+ */
20
+ let SimFaceCapture = class SimFaceCapture extends LitElement {
21
+ constructor() {
22
+ super(...arguments);
23
+ this.label = 'Take a selfie';
24
+ this.embedded = false;
25
+ this.active = false;
26
+ this.confirmLabel = 'Use this capture';
27
+ this.captureState = 'idle';
28
+ this.errorMessage = '';
29
+ this.feedbackMessage = 'Start a capture to see camera guidance here.';
30
+ this.feedbackTone = 'neutral';
31
+ this.previewUrl = '';
32
+ this.countdownProgress = 0;
33
+ this.qualityResult = null;
34
+ this.stream = null;
35
+ this.animationFrameId = null;
36
+ this.analysisInFlight = false;
37
+ this.lastAnalysisTimestamp = 0;
38
+ this.capturedBlob = null;
39
+ this.countdownStartedAt = null;
40
+ this.bestCaptureBlob = null;
41
+ this.bestCaptureScore = -1;
42
+ this.bestQualityResult = null;
43
+ }
44
+ disconnectedCallback() {
45
+ super.disconnectedCallback();
46
+ this.stopEmbeddedSession();
47
+ }
48
+ updated(changedProperties) {
49
+ if (!this.embedded || !changedProperties.has('active')) {
50
+ return;
51
+ }
52
+ if (this.active) {
53
+ void this.startEmbeddedCapture();
54
+ return;
55
+ }
56
+ this.stopEmbeddedSession();
57
+ this.resetEmbeddedState();
58
+ }
59
+ render() {
60
+ if (this.embedded) {
61
+ return html `
62
+ <div class="container embedded-shell">
63
+ ${this.renderEmbeddedState()}
64
+ </div>
65
+ `;
66
+ }
67
+ return html `
68
+ <div class="container">
69
+ ${this.renderPopupState()}
70
+ </div>
71
+ `;
72
+ }
73
+ async startCapture() {
74
+ if (this.embedded) {
75
+ this.active = true;
76
+ await this.updateComplete;
77
+ return;
78
+ }
79
+ await this.handlePopupCapture();
80
+ }
81
+ renderPopupState() {
82
+ switch (this.captureState) {
83
+ case 'idle':
84
+ return html `
85
+ <p>${this.label}</p>
86
+ <button class="btn btn-primary" @click=${this.handlePopupCapture}>
87
+ Open Camera
88
+ </button>
89
+ `;
90
+ case 'starting':
91
+ return html `
92
+ <p>Opening camera...</p>
93
+ <div class="spinner"></div>
94
+ `;
95
+ case 'error':
96
+ return html `
97
+ <div class="quality-msg quality-bad">${this.errorMessage}</div>
98
+ <button class="btn btn-primary" @click=${this.handlePopupRetake}>Try again</button>
99
+ <button class="btn btn-secondary" @click=${this.handlePopupCancel}>Cancel</button>
100
+ `;
101
+ default:
102
+ return html ``;
103
+ }
104
+ }
105
+ renderEmbeddedState() {
106
+ return html `
107
+ <p class="embedded-copy">${this.label}</p>
108
+
109
+ ${this.captureState === 'idle'
110
+ ? html `<p class="embedded-copy">Waiting for the host page to start capture.</p>`
111
+ : html `
112
+ <div class="stage">
113
+ <video
114
+ id="embedded-video"
115
+ class="video"
116
+ autoplay
117
+ muted
118
+ playsinline
119
+ ></video>
120
+ <img
121
+ class="preview-img ${this.captureState === 'preview' ? '' : 'hidden'}"
122
+ src=${this.previewUrl}
123
+ alt="Captured face preview"
124
+ />
125
+ <div
126
+ class="guide-overlay ${this.captureState === 'live' || this.captureState === 'starting' ? '' : 'hidden'}"
127
+ style=${`--capture-progress:${this.countdownProgress};`}
128
+ >
129
+ <svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
130
+ <path class="guide-mask" d=${CAPTURE_GUIDE_MASK_PATH}></path>
131
+ <path class="ring-outline" d=${CAPTURE_GUIDE_PATH}></path>
132
+ <path class="ring-progress" d=${CAPTURE_GUIDE_PATH} pathLength="100"></path>
133
+ </svg>
134
+ </div>
135
+ </div>
136
+ `}
137
+
138
+ <div class="quality-msg ${this.feedbackClass()}">
139
+ ${this.captureState === 'starting' ? 'Requesting camera access...' : this.feedbackMessage}
140
+ </div>
141
+
142
+ <div class="btn-row">
143
+ ${this.captureState === 'live'
144
+ ? html `
145
+ <button class="btn btn-secondary" @click=${this.handleEmbeddedManualCapture}>Take photo now</button>
146
+ <button class="btn btn-ghost" @click=${this.handleEmbeddedCancel}>Cancel</button>
147
+ `
148
+ : ''}
149
+ ${this.captureState === 'preview'
150
+ ? html `
151
+ <button class="btn btn-secondary" @click=${this.handleEmbeddedRetake}>Retake</button>
152
+ ${this.qualityResult?.passesQualityChecks === false
153
+ ? ''
154
+ : html `<button class="btn btn-primary" @click=${this.handleEmbeddedConfirm}>${this.confirmLabel}</button>`}
155
+ <button class="btn btn-ghost" @click=${this.handleEmbeddedCancel}>Cancel</button>
156
+ `
157
+ : ''}
158
+ ${this.captureState === 'error'
159
+ ? html `
160
+ <button class="btn btn-primary" @click=${this.startEmbeddedCapture}>Try again</button>
161
+ <button class="btn btn-ghost" @click=${this.handleEmbeddedCancel}>Cancel</button>
162
+ `
163
+ : ''}
164
+ </div>
165
+ `;
166
+ }
167
+ async handlePopupCapture() {
168
+ this.captureState = 'starting';
169
+ try {
170
+ const blob = await captureFromCamera();
171
+ if (!blob) {
172
+ this.dispatchCancelled();
173
+ this.captureState = 'idle';
174
+ return;
175
+ }
176
+ this.dispatchCaptured(blob);
177
+ this.resetPopupState();
178
+ }
179
+ catch (err) {
180
+ this.errorMessage = err instanceof Error ? err.message : 'Capture failed';
181
+ this.captureState = 'error';
182
+ this.dispatchError(this.errorMessage);
183
+ }
184
+ }
185
+ handlePopupRetake() {
186
+ this.resetPopupState();
187
+ void this.handlePopupCapture();
188
+ }
189
+ handlePopupCancel() {
190
+ this.dispatchCancelled();
191
+ this.resetPopupState();
192
+ }
193
+ async startEmbeddedCapture() {
194
+ if (!this.active || this.captureState === 'starting' || this.captureState === 'live') {
195
+ return;
196
+ }
197
+ this.stopEmbeddedSession();
198
+ this.resetEmbeddedState();
199
+ this.captureState = 'starting';
200
+ this.feedbackMessage = 'Requesting camera access...';
201
+ this.feedbackTone = 'neutral';
202
+ await this.updateComplete;
203
+ if (!navigator.mediaDevices?.getUserMedia) {
204
+ this.handleEmbeddedError(new Error('This browser does not support inline camera capture.'));
205
+ return;
206
+ }
207
+ try {
208
+ this.stream = await navigator.mediaDevices.getUserMedia({
209
+ video: { facingMode: { ideal: 'user' } },
210
+ audio: false,
211
+ });
212
+ this.captureState = 'live';
213
+ this.feedbackMessage = 'Center your face in the oval. We will capture automatically when framing looks good.';
214
+ await this.updateComplete;
215
+ const video = this.embeddedVideoElement;
216
+ if (!video) {
217
+ throw new Error('Inline camera preview could not be created.');
218
+ }
219
+ video.srcObject = this.stream;
220
+ await this.waitForVideoReady(video);
221
+ this.scheduleEmbeddedAnalysis();
222
+ }
223
+ catch (error) {
224
+ this.handleEmbeddedError(error);
225
+ }
226
+ }
227
+ scheduleEmbeddedAnalysis() {
228
+ if (this.captureState !== 'live' || !this.stream) {
229
+ return;
230
+ }
231
+ if (typeof window.requestAnimationFrame !== 'function' ||
232
+ typeof window.cancelAnimationFrame !== 'function') {
233
+ this.feedbackMessage = 'Automatic analysis is unavailable. Use Take photo now.';
234
+ this.feedbackTone = 'neutral';
235
+ return;
236
+ }
237
+ this.animationFrameId = window.requestAnimationFrame(async (timestamp) => {
238
+ if (this.captureState !== 'live' ||
239
+ this.analysisInFlight ||
240
+ timestamp - this.lastAnalysisTimestamp < AUTO_CAPTURE_ANALYSIS_INTERVAL_MS) {
241
+ this.scheduleEmbeddedAnalysis();
242
+ return;
243
+ }
244
+ const video = this.embeddedVideoElement;
245
+ if (!video) {
246
+ return;
247
+ }
248
+ this.lastAnalysisTimestamp = timestamp;
249
+ this.analysisInFlight = true;
250
+ try {
251
+ const qualityResult = await assessFaceQualityForVideo(video, timestamp);
252
+ this.qualityResult = qualityResult;
253
+ if (qualityResult.passesQualityChecks) {
254
+ if (this.countdownStartedAt === null) {
255
+ this.countdownStartedAt = timestamp;
256
+ this.countdownProgress = 0;
257
+ this.feedbackMessage = 'Great framing detected. Hold still while we pick the best frame.';
258
+ this.feedbackTone = 'success';
259
+ }
260
+ await this.considerBestFrame(video, qualityResult);
261
+ }
262
+ if (this.countdownStartedAt !== null) {
263
+ this.countdownProgress = Math.min((timestamp - this.countdownStartedAt) / AUTO_CAPTURE_COUNTDOWN_MS, 1);
264
+ this.feedbackMessage = autoCaptureCountdownMessage(timestamp, this.countdownStartedAt, qualityResult);
265
+ this.feedbackTone = qualityResult.passesQualityChecks ? 'success' : 'neutral';
266
+ if (this.countdownProgress >= 1) {
267
+ this.finishCountdownCapture();
268
+ return;
269
+ }
270
+ }
271
+ else {
272
+ this.feedbackMessage = qualityResult.message;
273
+ this.feedbackTone = 'neutral';
274
+ }
275
+ }
276
+ catch {
277
+ this.feedbackMessage = 'Automatic analysis is unavailable. Use Take photo now.';
278
+ this.feedbackTone = 'neutral';
279
+ return;
280
+ }
281
+ finally {
282
+ this.analysisInFlight = false;
283
+ }
284
+ this.scheduleEmbeddedAnalysis();
285
+ });
286
+ }
287
+ async captureEmbeddedFrame() {
288
+ const video = this.embeddedVideoElement;
289
+ if (!video || this.captureState !== 'live') {
290
+ return;
291
+ }
292
+ try {
293
+ const blob = await this.captureVideoFrame(video);
294
+ const qualityResult = await this.assessCapturedBlob(blob);
295
+ this.capturedBlob = blob;
296
+ this.qualityResult = qualityResult;
297
+ this.captureState = 'preview';
298
+ this.feedbackMessage = qualityResult?.message ?? 'Review this capture before continuing.';
299
+ this.feedbackTone = qualityResult
300
+ ? qualityResult.passesQualityChecks
301
+ ? 'success'
302
+ : 'error'
303
+ : 'neutral';
304
+ if (this.previewUrl) {
305
+ URL.revokeObjectURL(this.previewUrl);
306
+ }
307
+ this.previewUrl = URL.createObjectURL(blob);
308
+ this.countdownProgress = 0;
309
+ }
310
+ catch (error) {
311
+ this.handleEmbeddedError(error);
312
+ }
313
+ }
314
+ async considerBestFrame(video, qualityResult) {
315
+ if (qualityResult.captureScore <= this.bestCaptureScore) {
316
+ return;
317
+ }
318
+ const blob = await this.captureVideoFrame(video);
319
+ this.bestCaptureBlob = blob;
320
+ this.bestCaptureScore = qualityResult.captureScore;
321
+ this.bestQualityResult = qualityResult;
322
+ }
323
+ finishCountdownCapture() {
324
+ if (!this.bestCaptureBlob) {
325
+ void this.captureEmbeddedFrame();
326
+ return;
327
+ }
328
+ this.capturedBlob = this.bestCaptureBlob;
329
+ this.qualityResult = this.bestQualityResult;
330
+ this.captureState = 'preview';
331
+ this.feedbackMessage = autoCaptureCompleteMessage(this.bestQualityResult);
332
+ this.feedbackTone = 'success';
333
+ if (this.previewUrl) {
334
+ URL.revokeObjectURL(this.previewUrl);
335
+ }
336
+ this.previewUrl = URL.createObjectURL(this.bestCaptureBlob);
337
+ this.countdownProgress = 1;
338
+ }
339
+ async assessCapturedBlob(blob) {
340
+ try {
341
+ const image = await blobToImage(blob);
342
+ return await assessFaceQuality(image);
343
+ }
344
+ catch {
345
+ return null;
346
+ }
347
+ }
348
+ handleEmbeddedManualCapture() {
349
+ void this.captureEmbeddedFrame();
350
+ }
351
+ handleEmbeddedRetake() {
352
+ this.capturedBlob = null;
353
+ this.qualityResult = null;
354
+ this.captureState = 'live';
355
+ if (this.previewUrl) {
356
+ URL.revokeObjectURL(this.previewUrl);
357
+ this.previewUrl = '';
358
+ }
359
+ this.feedbackMessage = 'Ready to capture again.';
360
+ this.feedbackTone = 'neutral';
361
+ this.resetCountdown();
362
+ this.resumeEmbeddedVideo();
363
+ this.scheduleEmbeddedAnalysis();
364
+ }
365
+ handleEmbeddedConfirm() {
366
+ if (!this.capturedBlob) {
367
+ return;
368
+ }
369
+ const blob = this.capturedBlob;
370
+ this.active = false;
371
+ this.stopEmbeddedSession();
372
+ this.resetEmbeddedState();
373
+ this.dispatchCaptured(blob);
374
+ }
375
+ handleEmbeddedCancel() {
376
+ this.active = false;
377
+ this.stopEmbeddedSession();
378
+ this.resetEmbeddedState();
379
+ this.dispatchCancelled();
380
+ }
381
+ handleEmbeddedError(error) {
382
+ this.stopEmbeddedSession();
383
+ this.errorMessage = error instanceof Error ? error.message : 'Capture failed';
384
+ this.captureState = 'error';
385
+ this.feedbackMessage = this.errorMessage;
386
+ this.feedbackTone = 'error';
387
+ this.dispatchError(this.errorMessage);
388
+ }
389
+ stopEmbeddedSession() {
390
+ if (this.animationFrameId !== null) {
391
+ window.cancelAnimationFrame(this.animationFrameId);
392
+ this.animationFrameId = null;
393
+ }
394
+ if (this.stream) {
395
+ this.stream.getTracks().forEach((track) => track.stop());
396
+ this.stream = null;
397
+ }
398
+ const video = this.embeddedVideoElement;
399
+ if (video) {
400
+ video.srcObject = null;
401
+ }
402
+ this.analysisInFlight = false;
403
+ this.lastAnalysisTimestamp = 0;
404
+ this.resetCountdown();
405
+ }
406
+ resetEmbeddedState() {
407
+ if (this.previewUrl) {
408
+ URL.revokeObjectURL(this.previewUrl);
409
+ this.previewUrl = '';
410
+ }
411
+ this.captureState = 'idle';
412
+ this.errorMessage = '';
413
+ this.feedbackMessage = 'Start a capture to see camera guidance here.';
414
+ this.feedbackTone = 'neutral';
415
+ this.countdownProgress = 0;
416
+ this.qualityResult = null;
417
+ this.capturedBlob = null;
418
+ }
419
+ resetPopupState() {
420
+ this.captureState = 'idle';
421
+ this.errorMessage = '';
422
+ }
423
+ dispatchCaptured(blob) {
424
+ this.dispatchEvent(new CustomEvent('simface-captured', {
425
+ detail: { imageBlob: blob },
426
+ bubbles: true,
427
+ composed: true,
428
+ }));
429
+ }
430
+ dispatchCancelled() {
431
+ this.dispatchEvent(new CustomEvent('simface-cancelled', {
432
+ bubbles: true,
433
+ composed: true,
434
+ }));
435
+ }
436
+ dispatchError(message) {
437
+ this.dispatchEvent(new CustomEvent('simface-error', {
438
+ detail: { error: message },
439
+ bubbles: true,
440
+ composed: true,
441
+ }));
442
+ }
443
+ feedbackClass() {
444
+ if (this.feedbackTone === 'success') {
445
+ return 'quality-good';
446
+ }
447
+ if (this.feedbackTone === 'error') {
448
+ return 'quality-bad';
449
+ }
450
+ return 'quality-neutral';
451
+ }
452
+ waitForVideoReady(video) {
453
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
454
+ return video.play().then(() => undefined);
455
+ }
456
+ return new Promise((resolve, reject) => {
457
+ const handleReady = () => {
458
+ cleanup();
459
+ video.play().then(() => resolve()).catch(reject);
460
+ };
461
+ const handleError = () => {
462
+ cleanup();
463
+ reject(new Error('Failed to start the inline camera preview.'));
464
+ };
465
+ const cleanup = () => {
466
+ video.removeEventListener('loadedmetadata', handleReady);
467
+ video.removeEventListener('error', handleError);
468
+ };
469
+ video.addEventListener('loadedmetadata', handleReady, { once: true });
470
+ video.addEventListener('error', handleError, { once: true });
471
+ });
472
+ }
473
+ captureVideoFrame(video) {
474
+ if (!video.videoWidth || !video.videoHeight) {
475
+ return Promise.reject(new Error('Camera preview is not ready yet.'));
476
+ }
477
+ const canvas = document.createElement('canvas');
478
+ canvas.width = video.videoWidth;
479
+ canvas.height = video.videoHeight;
480
+ const context = canvas.getContext('2d');
481
+ if (!context) {
482
+ return Promise.reject(new Error('Failed to initialize camera capture.'));
483
+ }
484
+ context.drawImage(video, 0, 0, canvas.width, canvas.height);
485
+ return new Promise((resolve, reject) => {
486
+ canvas.toBlob((blob) => {
487
+ if (!blob) {
488
+ reject(new Error('Failed to capture an image.'));
489
+ return;
490
+ }
491
+ resolve(blob);
492
+ }, 'image/jpeg', 0.92);
493
+ });
494
+ }
495
+ resumeEmbeddedVideo() {
496
+ const video = this.embeddedVideoElement;
497
+ if (!video) {
498
+ return;
499
+ }
500
+ void video.play().catch(() => {
501
+ // Ignore replay failures here; the initial preview startup path already errors loudly.
502
+ });
503
+ }
504
+ resetCountdown() {
505
+ this.countdownStartedAt = null;
506
+ this.countdownProgress = 0;
507
+ this.bestCaptureBlob = null;
508
+ this.bestCaptureScore = -1;
509
+ this.bestQualityResult = null;
510
+ }
511
+ };
512
+ SimFaceCapture.styles = css `
513
+ :host {
514
+ display: block;
515
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
516
+ max-width: 400px;
517
+ margin: 0 auto;
518
+ text-align: center;
519
+ }
520
+
521
+ :host([embedded]) {
522
+ max-width: none;
523
+ margin: 0;
524
+ text-align: left;
525
+ }
526
+
527
+ .container {
528
+ padding: 16px;
529
+ border: 1px solid #e0e0e0;
530
+ border-radius: 12px;
531
+ background: #fafafa;
532
+ }
533
+
534
+ .embedded-shell {
535
+ display: flex;
536
+ flex-direction: column;
537
+ gap: 16px;
538
+ }
539
+
540
+ .embedded-copy {
541
+ margin: 0;
542
+ color: #334155;
543
+ }
544
+
545
+ .stage {
546
+ position: relative;
547
+ overflow: hidden;
548
+ width: min(100%, 420px);
549
+ aspect-ratio: 3 / 4;
550
+ border-radius: 22px;
551
+ background:
552
+ radial-gradient(circle at top, rgba(56, 189, 248, 0.16), transparent 30%),
553
+ linear-gradient(180deg, #0f172a, #020617);
554
+ box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.2);
555
+ align-self: center;
556
+ }
557
+
558
+ .video,
559
+ .preview-img {
560
+ width: 100%;
561
+ height: 100%;
562
+ object-fit: cover;
563
+ }
564
+
565
+ .preview-img {
566
+ position: absolute;
567
+ inset: 0;
568
+ z-index: 2;
569
+ }
570
+
571
+ .guide-overlay {
572
+ position: absolute;
573
+ inset: 0;
574
+ pointer-events: none;
575
+ z-index: 3;
576
+ }
577
+
578
+ .guide-overlay svg {
579
+ width: 100%;
580
+ height: 100%;
581
+ display: block;
582
+ }
583
+
584
+ .guide-mask {
585
+ fill: rgba(51, 65, 85, 0.75);
586
+ fill-rule: evenodd;
587
+ }
588
+
589
+ .ring-outline {
590
+ fill: none;
591
+ stroke: rgba(255, 255, 255, 0.92);
592
+ stroke-width: 2.8;
593
+ stroke-linecap: round;
594
+ stroke-linejoin: round;
595
+ }
596
+
597
+ .ring-progress {
598
+ fill: none;
599
+ stroke: #22c55e;
600
+ stroke-width: 2.8;
601
+ stroke-linecap: round;
602
+ stroke-linejoin: round;
603
+ stroke-dasharray: 100;
604
+ stroke-dashoffset: calc(100 - var(--capture-progress, 0) * 100);
605
+ transition: stroke-dashoffset 0.14s linear;
606
+ }
607
+
608
+ .btn-row {
609
+ display: flex;
610
+ flex-wrap: wrap;
611
+ gap: 12px;
612
+ }
613
+
614
+ .preview-img-inline {
615
+ max-width: 100%;
616
+ border-radius: 8px;
617
+ margin: 12px 0;
618
+ }
619
+
620
+ .btn {
621
+ display: inline-flex;
622
+ align-items: center;
623
+ justify-content: center;
624
+ padding: 12px 24px;
625
+ margin: 8px 4px 0 0;
626
+ border: none;
627
+ border-radius: 999px;
628
+ font-size: 16px;
629
+ font-weight: 600;
630
+ cursor: pointer;
631
+ transition: background-color 0.2s;
632
+ }
633
+
634
+ .btn-primary {
635
+ background: #2563eb;
636
+ color: white;
637
+ }
638
+
639
+ .btn-primary:hover {
640
+ background: #1d4ed8;
641
+ }
642
+
643
+ .btn-primary:disabled {
644
+ background: #93c5fd;
645
+ cursor: not-allowed;
646
+ }
647
+
648
+ .btn-secondary {
649
+ background: #e5e7eb;
650
+ color: #374151;
651
+ }
652
+
653
+ .btn-secondary:hover {
654
+ background: #d1d5db;
655
+ }
656
+
657
+ .btn-ghost {
658
+ background: #e2e8f0;
659
+ color: #0f172a;
660
+ }
661
+
662
+ .quality-msg {
663
+ padding: 10px 14px;
664
+ border-radius: 14px;
665
+ margin: 8px 0 0;
666
+ font-size: 14px;
667
+ font-weight: 600;
668
+ }
669
+
670
+ .quality-good {
671
+ background: #dcfce7;
672
+ color: #166534;
673
+ }
674
+
675
+ .quality-bad {
676
+ background: #fef2f2;
677
+ color: #991b1b;
678
+ }
679
+
680
+ .quality-neutral {
681
+ background: #e2e8f0;
682
+ color: #0f172a;
683
+ }
684
+
685
+ .spinner {
686
+ display: inline-block;
687
+ width: 24px;
688
+ height: 24px;
689
+ border: 3px solid #e5e7eb;
690
+ border-top: 3px solid #2563eb;
691
+ border-radius: 50%;
692
+ animation: spin 0.8s linear infinite;
693
+ margin: 12px auto;
694
+ }
695
+
696
+ .hidden {
697
+ display: none;
698
+ }
699
+
700
+ @keyframes spin {
701
+ to {
702
+ transform: rotate(360deg);
703
+ }
704
+ }
705
+ `;
706
+ __decorate([
707
+ property({ type: String })
708
+ ], SimFaceCapture.prototype, "label", void 0);
709
+ __decorate([
710
+ property({ type: Boolean, reflect: true })
711
+ ], SimFaceCapture.prototype, "embedded", void 0);
712
+ __decorate([
713
+ property({ type: Boolean, reflect: true })
714
+ ], SimFaceCapture.prototype, "active", void 0);
715
+ __decorate([
716
+ property({ type: String, attribute: 'confirm-label' })
717
+ ], SimFaceCapture.prototype, "confirmLabel", void 0);
718
+ __decorate([
719
+ state()
720
+ ], SimFaceCapture.prototype, "captureState", void 0);
721
+ __decorate([
722
+ state()
723
+ ], SimFaceCapture.prototype, "errorMessage", void 0);
724
+ __decorate([
725
+ state()
726
+ ], SimFaceCapture.prototype, "feedbackMessage", void 0);
727
+ __decorate([
728
+ state()
729
+ ], SimFaceCapture.prototype, "feedbackTone", void 0);
730
+ __decorate([
731
+ state()
732
+ ], SimFaceCapture.prototype, "previewUrl", void 0);
733
+ __decorate([
734
+ state()
735
+ ], SimFaceCapture.prototype, "countdownProgress", void 0);
736
+ __decorate([
737
+ state()
738
+ ], SimFaceCapture.prototype, "qualityResult", void 0);
739
+ __decorate([
740
+ query('#embedded-video')
741
+ ], SimFaceCapture.prototype, "embeddedVideoElement", void 0);
742
+ SimFaceCapture = __decorate([
743
+ customElement('simface-capture')
744
+ ], SimFaceCapture);
745
+ export { SimFaceCapture };
746
+ //# sourceMappingURL=simface-capture.js.map