@simprints/simface-sdk 0.9.1 → 0.11.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # SimFace SDK
2
2
 
3
- SimFace SDK provides facial recognition for web-based KYC workflows. It is a drop-in Web Component and JavaScript API that handles camera capture, face quality validation, and communication with the SimFace backend.
3
+ SimFace SDK provides facial recognition for web-based KYC workflows. It exposes one primary JavaScript API for enrollment and verification, plus a lower-level Web Component for advanced UI control.
4
4
 
5
5
  Works in: all modern browsers, WhatsApp in-app browser, and mobile WebViews.
6
6
 
@@ -8,7 +8,20 @@ This repository is the public frontend SDK and demo repo for SimFace. For fronte
8
8
 
9
9
  The backend API, infrastructure, and TensorFlow Lite runtime live in the separate private backend repository.
10
10
 
11
- The capture flow is planned explicitly as: auto camera -> manual camera -> media picker. Hosts can keep the default popup experience or opt into embedded capture with the same fallback policy.
11
+ The capture flow is planned explicitly as: auto camera -> manual camera -> media picker. The primary API supports two UI modes:
12
+ - popup capture: the SDK opens and manages its own modal capture flow
13
+ - embedded capture: the SDK runs capture inside a host-provided `simface-capture` element
14
+
15
+ ## Face Quality Checks
16
+
17
+ The SDK automatically performs these checks on captured images before submission:
18
+
19
+ 1. Face presence - at least one face must be detected.
20
+ 2. Single face - only one face should be in the frame.
21
+ 3. Face size - face must not be too close or too far.
22
+ 4. Centering - face must be approximately centered in the frame.
23
+
24
+ If a check fails, the user is prompted with specific guidance and asked to retake the photo.
12
25
 
13
26
  ## Quick Start
14
27
 
@@ -75,55 +88,138 @@ if (result.match) {
75
88
  }
76
89
  ```
77
90
 
78
- ## Try the local demo
91
+ ### 5. Choose the capture UI mode
79
92
 
80
- Build the SDK at the repository root, then run the demo:
93
+ `enroll()` and `verify()` are the main SDK entry points. They both use the same capture workflow and backend API. The only UI difference is where the capture UI is rendered.
81
94
 
82
- ```bash
83
- npm install
84
- npm run build
95
+ Both functions accept:
96
+ - `workflowOptions`: optional capture behavior that applies to both popup and embedded flows
97
+ - `captureElement`: optional existing `simface-capture` element; if this argument is present, the SDK uses embedded mode
85
98
 
86
- cd demo
87
- npm install
88
- npm run dev
99
+ #### Popup capture
100
+
101
+ If you omit `captureElement`, the SDK opens its popup capture UI:
102
+
103
+ ```javascript
104
+ const workflowOptions = {
105
+ capturePreference: 'auto-preferred',
106
+ allowMediaPickerFallback: true,
107
+ };
108
+
109
+ const enrollResult = await enroll(config, 'unique-user-id', workflowOptions);
110
+ const verifyResult = await verify(config, 'unique-user-id', workflowOptions);
89
111
  ```
90
112
 
91
- The demo runs at `http://localhost:4173` and consumes the built SDK artifact from `dist/`. To enable HTTPS (required for camera access from other devices on the local network), set `DEMO_USE_HTTPS=true` before starting the demo.
113
+ #### Embedded capture
92
114
 
93
- ## Capture strategy
115
+ If you want capture inline in your page, create a `simface-capture` element and pass it as `captureElement`. The SDK still owns the capture lifecycle; it just renders the UI inline instead of in a popup.
94
116
 
95
- The top-level SDK helpers accept an optional third `captureOptions` argument so the host can choose popup vs embedded capture and tune the fallback chain:
117
+ ```html
118
+ <simface-capture
119
+ embedded
120
+ capture-preference="auto-preferred"
121
+ label="Take a selfie for verification"
122
+ idle-feedback-label="Start verification to see camera guidance here."
123
+ capture-label="Snap photo"
124
+ retake-label="Take another"
125
+ confirm-label="Use this photo"
126
+ retry-label="Start over"
127
+ ></simface-capture>
128
+ ```
96
129
 
97
130
  ```javascript
98
- import { enroll } from '@simprints/simface-sdk';
99
-
100
- const result = await enroll(config, 'unique-user-id', {
101
- presentation: 'embedded',
102
- container: '#capture-slot',
131
+ const workflowOptions = {
103
132
  capturePreference: 'auto-preferred',
104
133
  allowMediaPickerFallback: true,
105
- label: 'Capture a face for enrollment',
106
- confirmLabel: 'Confirm enrollment capture',
107
- });
134
+ };
135
+
136
+ const captureElement = document.querySelector('simface-capture');
137
+
138
+ const enrollResult = await enroll(config, 'unique-user-id', workflowOptions, captureElement);
139
+ const verifyResult = await verify(config, 'unique-user-id', workflowOptions, captureElement);
108
140
  ```
109
141
 
110
- Supported capture planning options:
142
+ | workflowOptions | Type | Default | Notes |
143
+ |--------|------|---------|-------|
144
+ | `capturePreference` | `'auto-preferred' \| 'manual-only'` | `'auto-preferred'` | Controls auto vs manual shutter |
145
+ | `allowMediaPickerFallback` | `boolean` | `true` | Falls back to file picker if camera is unavailable |
146
+
147
+ ## API Reference
148
+
149
+ ### Primary SDK API
150
+
151
+ The main integration surface is:
152
+ - `enroll(config, clientId, workflowOptions?, captureElement?)`
153
+ - `verify(config, clientId, workflowOptions?, captureElement?)`
154
+
155
+ These functions:
156
+ - run the camera capture workflow
157
+ - manage popup or embedded capture UI
158
+ - perform face quality validation
159
+ - call the backend API for enrollment or verification
160
+
161
+ ### `enroll(config, clientId, workflowOptions?, captureElement?): Promise<EnrollResult>`
162
+
163
+ Opens the camera, captures a face image with quality validation, and enrolls the user.
164
+
165
+ Parameters:
166
+
167
+ | Parameter | Type | Description |
168
+ |-----------|------|-------------|
169
+ | `config` | `SimFaceConfig` | SDK configuration (`apiUrl`, `projectId`, `apiKey`) |
170
+ | `clientId` | `string` | Unique identifier for the user |
171
+ | `workflowOptions` | `SimFaceWorkflowOptions` | Optional popup/embedded-agnostic capture behavior |
172
+ | `captureElement` | `SimFaceCaptureElement` | Optional embedded `simface-capture` element |
173
+
174
+ Returns: `EnrollResult`
175
+
176
+ ### `verify(config, clientId, workflowOptions?, captureElement?): Promise<VerifyResult>`
177
+
178
+ Opens the camera, captures a face image, and verifies against the enrolled face.
179
+
180
+ Parameters:
181
+
182
+ | Parameter | Type | Description |
183
+ |-----------|------|-------------|
184
+ | `config` | `SimFaceConfig` | SDK configuration (`apiUrl`, `projectId`, `apiKey`) |
185
+ | `clientId` | `string` | Unique identifier for the user |
186
+ | `workflowOptions` | `SimFaceWorkflowOptions` | Optional popup/embedded-agnostic capture behavior |
187
+ | `captureElement` | `SimFaceCaptureElement` | Optional embedded `simface-capture` element |
188
+
189
+ Returns: `VerifyResult`
190
+
191
+ ### `SimFaceAPIClient` and the backend REST interface
192
+
193
+ `SimFaceAPIClient` is the lower-level HTTP client used internally by `enroll()` and `verify()`. Use it when you want direct control over when capture happens and when backend calls are made.
111
194
 
112
- - `presentation: 'popup' | 'embedded'`
113
- - `capturePreference: 'auto-preferred' | 'manual-only'`
114
- - `allowMediaPickerFallback: boolean`
115
- - `container: HTMLElement | string` (required for top-level embedded capture)
116
- - `label` / `confirmLabel`
195
+ Typical cases for using `SimFaceAPIClient` directly:
196
+ - advanced UI flows driven by your own application state
197
+ - direct use of the `simface-capture` component
198
+ - custom orchestration where capture and backend submission happen in separate steps
117
199
 
118
- ## Web Component
200
+ At a high level:
201
+ - `enroll()` and `verify()` = capture UI + quality checks + backend submission
202
+ - `SimFaceAPIClient` = backend submission only
119
203
 
120
- For more control over the UI, use the `<simface-capture>` Web Component directly:
204
+ `SimFaceAPIClient` maps directly to the backend REST interface:
205
+ - `validateAPIKey()` -> `POST /api/v1/auth/validate`
206
+ - `enroll(clientId, imageBlob)` -> `POST /api/v1/enroll`
207
+ - `verify(clientId, imageBlob)` -> `POST /api/v1/verify`
208
+
209
+ ## Advanced: Direct `simface-capture` control
210
+
211
+ Use the `simface-capture` Web Component directly when you want the host application to manage capture state itself instead of letting `enroll()` or `verify()` orchestrate it. In this mode, the component is also the source of truth for embedded UI copy.
121
212
 
122
213
  ```html
123
214
  <simface-capture
124
215
  embedded
125
216
  capture-preference="auto-preferred"
126
217
  label="Take a selfie for verification"
218
+ idle-feedback-label="Start verification to see camera guidance here."
219
+ capture-label="Snap photo"
220
+ retake-label="Take another"
221
+ confirm-label="Use this photo"
222
+ retry-label="Start over"
127
223
  ></simface-capture>
128
224
 
129
225
  <script type="module">
@@ -150,16 +246,30 @@ For more control over the UI, use the `<simface-capture>` Web Component directly
150
246
  captureEl.addEventListener('simface-error', (e) => {
151
247
  console.error('Capture error:', e.detail.error);
152
248
  });
249
+
250
+ await captureEl.startCapture();
153
251
  </script>
154
252
  ```
155
253
 
254
+ In this advanced flow:
255
+ 1. the host renders the component
256
+ 2. the host starts capture with `startCapture()` or by setting `active = true`
257
+ 3. the component emits capture events
258
+ 4. the host decides what backend call to make with `SimFaceAPIClient`
259
+
260
+ This is more flexible, but it also means the host owns more of the workflow.
261
+
156
262
  ### Component Attributes
157
263
 
158
264
  | Attribute | Type | Default | Description |
159
265
  |-----------|------|---------|-------------|
160
- | `label` | String | `"Take a selfie"` | Instructional text shown on the capture button |
266
+ | `label` | String | `"Capturing Face"` | Primary instructional text shown by the component |
267
+ | `idle-feedback-label` | String | `"Start a capture to see camera guidance here."` | Idle guidance text shown in the feedback area before capture begins |
161
268
  | `embedded` | Boolean | `false` | Runs the component inline instead of delegating to the popup capture service |
162
- | `confirm-label` | String | `"Use this capture"` | Confirm button label used in preview state |
269
+ | `capture-label` | String | `"Take photo"` | Manual capture button label |
270
+ | `retake-label` | String | `"Retake"` | Preview retake button label |
271
+ | `confirm-label` | String | `"Accept"` | Confirm button label used in preview state |
272
+ | `retry-label` | String | `"Try again"` | Error-state retry button label |
163
273
  | `capture-preference` | `"auto-preferred" \| "manual-only"` | `"auto-preferred"` | Whether auto capture should be preferred or disabled |
164
274
  | `allow-media-picker-fallback` | Boolean | `true` | Whether the component may fall back to the media picker if camera capture is unavailable |
165
275
 
@@ -171,23 +281,31 @@ For more control over the UI, use the `<simface-capture>` Web Component directly
171
281
  | `simface-cancelled` | - | Fires when the user cancels the capture flow |
172
282
  | `simface-error` | `{ error: string }` | Fires on capture/detection errors |
173
283
 
174
- ## API Reference
284
+ ## Type Definitions
175
285
 
176
- ### `enroll(config, clientId, captureOptions?): Promise<EnrollResult>`
286
+ ### `SimFaceConfig`
177
287
 
178
- Opens the camera, captures a face image with quality validation, and enrolls the user.
288
+ ```typescript
289
+ interface SimFaceConfig {
290
+ apiUrl: string;
291
+ projectId: string;
292
+ apiKey: string;
293
+ }
294
+ ```
179
295
 
180
- Parameters:
296
+ ### `SimFaceWorkflowOptions`
181
297
 
182
- | Parameter | Type | Description |
183
- |-----------|------|-------------|
184
- | `config` | `SimFaceConfig` | SDK configuration (`apiUrl`, `projectId`, `apiKey`) |
185
- | `clientId` | `string` | Unique identifier for the user |
186
- | `captureOptions` | `SimFaceCaptureOptions` | Optional capture presentation/fallback overrides |
298
+ ```typescript
299
+ interface SimFaceWorkflowOptions {
300
+ capturePreference?: 'auto-preferred' | 'manual-only';
301
+ allowMediaPickerFallback?: boolean;
302
+ }
303
+ ```
304
+
305
+ ### `EnrollResult`
187
306
 
188
- Returns `EnrollResult`:
189
307
  ```typescript
190
- {
308
+ interface EnrollResult {
191
309
  success: boolean;
192
310
  clientId: string;
193
311
  message?: string;
@@ -195,21 +313,10 @@ Returns `EnrollResult`:
195
313
  }
196
314
  ```
197
315
 
198
- ### `verify(config, clientId, captureOptions?): Promise<VerifyResult>`
199
-
200
- Opens the camera, captures a face image, and verifies against the enrolled face.
201
-
202
- Parameters:
203
-
204
- | Parameter | Type | Description |
205
- |-----------|------|-------------|
206
- | `config` | `SimFaceConfig` | SDK configuration (`apiUrl`, `projectId`, `apiKey`) |
207
- | `clientId` | `string` | Unique identifier for the user |
208
- | `captureOptions` | `SimFaceCaptureOptions` | Optional capture presentation/fallback overrides |
316
+ ### `VerifyResult`
209
317
 
210
- Returns `VerifyResult`:
211
318
  ```typescript
212
- {
319
+ interface VerifyResult {
213
320
  match: boolean;
214
321
  score: number;
215
322
  threshold: number;
@@ -218,29 +325,6 @@ Returns `VerifyResult`:
218
325
  }
219
326
  ```
220
327
 
221
- ### `SimFaceConfig`
222
-
223
- ```typescript
224
- {
225
- apiUrl: string;
226
- projectId: string;
227
- apiKey: string;
228
- }
229
- ```
230
-
231
- ### `SimFaceCaptureOptions`
232
-
233
- ```typescript
234
- {
235
- presentation?: 'popup' | 'embedded';
236
- capturePreference?: 'auto-preferred' | 'manual-only';
237
- allowMediaPickerFallback?: boolean;
238
- container?: HTMLElement | string;
239
- label?: string;
240
- confirmLabel?: string;
241
- }
242
- ```
243
-
244
328
  ## Backend API Endpoints
245
329
 
246
330
  For clients integrating directly with the REST API:
@@ -268,17 +352,6 @@ Health check endpoint.
268
352
 
269
353
  Response: `{ "status": "ok" }`
270
354
 
271
- ## Face Quality Checks
272
-
273
- The SDK automatically performs these checks on captured images before submission:
274
-
275
- 1. Face presence - at least one face must be detected.
276
- 2. Single face - only one face should be in the frame.
277
- 3. Face size - face must not be too close or too far.
278
- 4. Centering - face must be approximately centered in the frame.
279
-
280
- If a check fails, the user is prompted with specific guidance and asked to retake the photo.
281
-
282
355
  ## Browser Compatibility
283
356
 
284
357
  | Browser | Camera Capture | Face Detection |
@@ -290,3 +363,18 @@ If a check fails, the user is prompted with specific guidance and asked to retak
290
363
  | Samsung Internet | Yes | Yes |
291
364
 
292
365
  > Note: In WhatsApp's in-app browser, the camera opens via the device's native camera app rather than an in-browser preview. Face quality checks run after the photo is taken.
366
+
367
+ ## Try the local demo
368
+
369
+ Build the SDK at the repository root, then run the demo:
370
+
371
+ ```bash
372
+ npm install
373
+ npm run build
374
+
375
+ cd demo
376
+ npm install
377
+ npm run dev
378
+ ```
379
+
380
+ The demo runs at `http://localhost:4173` and consumes the built SDK artifact from `dist/`. To enable HTTPS (required for camera access from other devices on the local network), set `DEMO_USE_HTTPS=true` before starting the demo.
@@ -10,9 +10,13 @@ import type { CapturePreference } from '../types/index.js';
10
10
  */
11
11
  export declare class SimFaceCapture extends LitElement {
12
12
  label: string;
13
+ idleFeedbackLabel: string;
13
14
  embedded: boolean;
14
15
  active: boolean;
15
16
  confirmLabel: string;
17
+ captureLabel: string;
18
+ retakeLabel: string;
19
+ retryLabel: string;
16
20
  capturePreference: CapturePreference;
17
21
  allowMediaPickerFallback: boolean;
18
22
  private captureState;
@@ -31,6 +35,7 @@ export declare class SimFaceCapture extends LitElement {
31
35
  private pendingActiveSync;
32
36
  static styles: import("lit").CSSResult;
33
37
  disconnectedCallback(): void;
38
+ protected willUpdate(changedProperties: Map<string, unknown>): void;
34
39
  updated(changedProperties: Map<string, unknown>): void;
35
40
  render(): import("lit-html").TemplateResult<1>;
36
41
  startCapture(): Promise<void>;
@@ -8,7 +8,7 @@ import { LitElement, html, css } from 'lit';
8
8
  import { customElement, property, query, state } from 'lit/decorators.js';
9
9
  import { assessFaceQuality } from '../services/face-detection.js';
10
10
  import { CAPTURE_GUIDE_MASK_PATH, CAPTURE_GUIDE_PATH, } from '../shared/auto-capture.js';
11
- import { buildCapturePlan, normalizeCaptureOptions, resolveCaptureCapabilities, } from '../shared/capture-flow.js';
11
+ import { buildCapturePlan, normalizeCaptureOptions, resolveCaptureCapabilities, DEFAULT_CAPTURE_LABEL, DEFAULT_IDLE_FEEDBACK_LABEL, DEFAULT_LABEL, DEFAULT_CONFIRM_LABEL, DEFAULT_RETAKE_LABEL, DEFAULT_RETRY_LABEL, } from '../shared/capture-flow.js';
12
12
  import { CameraCaptureSessionController, } from '../shared/capture-session.js';
13
13
  import { CameraAccessError, blobToImage, captureFromFileInput, openUserFacingCameraStream, } from '../shared/capture-runtime.js';
14
14
  /**
@@ -22,15 +22,19 @@ import { CameraAccessError, blobToImage, captureFromFileInput, openUserFacingCam
22
22
  let SimFaceCapture = class SimFaceCapture extends LitElement {
23
23
  constructor() {
24
24
  super(...arguments);
25
- this.label = 'Take a selfie';
25
+ this.label = DEFAULT_LABEL;
26
+ this.idleFeedbackLabel = DEFAULT_IDLE_FEEDBACK_LABEL;
26
27
  this.embedded = false;
27
28
  this.active = false;
28
- this.confirmLabel = 'Use this capture';
29
+ this.confirmLabel = DEFAULT_CONFIRM_LABEL;
30
+ this.captureLabel = DEFAULT_CAPTURE_LABEL;
31
+ this.retakeLabel = DEFAULT_RETAKE_LABEL;
32
+ this.retryLabel = DEFAULT_RETRY_LABEL;
29
33
  this.capturePreference = 'auto-preferred';
30
34
  this.allowMediaPickerFallback = true;
31
35
  this.captureState = 'idle';
32
36
  this.errorMessage = '';
33
- this.feedbackMessage = 'Start a capture to see camera guidance here.';
37
+ this.feedbackMessage = DEFAULT_IDLE_FEEDBACK_LABEL;
34
38
  this.feedbackTone = 'neutral';
35
39
  this.previewUrl = '';
36
40
  this.qualityResult = null;
@@ -46,6 +50,11 @@ let SimFaceCapture = class SimFaceCapture extends LitElement {
46
50
  super.disconnectedCallback();
47
51
  this.stopSession();
48
52
  }
53
+ willUpdate(changedProperties) {
54
+ if (changedProperties.has('idleFeedbackLabel') && this.captureState === 'idle') {
55
+ this.feedbackMessage = this.idleFeedbackLabel;
56
+ }
57
+ }
49
58
  updated(changedProperties) {
50
59
  if (!changedProperties.has('active') || this.pendingActiveSync) {
51
60
  return;
@@ -78,11 +87,20 @@ let SimFaceCapture = class SimFaceCapture extends LitElement {
78
87
  await this.beginCapture();
79
88
  }
80
89
  renderCaptureState() {
90
+ const showClose = this.captureState !== 'idle';
81
91
  return html `
82
- <p class="capture-copy">${this.label}</p>
92
+ ${showClose
93
+ ? html `<button class="close-btn" data-simface-action="cancel" @click=${this.handleCancel} aria-label="Close">
94
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
95
+ <line x1="18" y1="6" x2="6" y2="18"></line>
96
+ <line x1="6" y1="6" x2="18" y2="18"></line>
97
+ </svg>
98
+ </button>`
99
+ : ''}
83
100
 
101
+ <p class="capture-copy">${this.label}</p>
84
102
  ${this.captureState === 'idle'
85
- ? html `<p class="capture-copy">Waiting for the host page to start capture.</p>`
103
+ ? ''
86
104
  : html `
87
105
  <div class="stage">
88
106
  <video
@@ -117,24 +135,21 @@ let SimFaceCapture = class SimFaceCapture extends LitElement {
117
135
  ${this.captureState === 'live'
118
136
  ? html `
119
137
  ${this.captureMode === 'manual'
120
- ? html `<button class="btn btn-secondary" data-simface-action="capture" ?disabled=${!this.canTakePhoto} @click=${this.handleManualCapture}>Take photo</button>`
138
+ ? html `<button class="btn btn-primary" data-simface-action="capture" ?disabled=${!this.canTakePhoto} @click=${this.handleManualCapture}>${this.captureLabel}</button>`
121
139
  : ''}
122
- <button class="btn btn-ghost" data-simface-action="cancel" @click=${this.handleCancel}>Cancel</button>
123
140
  `
124
141
  : ''}
125
142
  ${this.captureState === 'preview'
126
143
  ? html `
127
- <button class="btn btn-secondary" data-simface-action="retake" @click=${this.handleRetake}>Retake</button>
144
+ <button class="btn btn-retake" data-simface-action="retake" @click=${this.handleRetake}>${this.retakeLabel}</button>
128
145
  ${this.qualityResult?.passesQualityChecks === false
129
146
  ? ''
130
- : html `<button class="btn btn-primary" data-simface-action="confirm" @click=${this.handleConfirm}>${this.confirmLabel}</button>`}
131
- <button class="btn btn-ghost" data-simface-action="cancel" @click=${this.handleCancel}>Cancel</button>
147
+ : html `<button class="btn btn-confirm" data-simface-action="confirm" @click=${this.handleConfirm}>${this.confirmLabel}</button>`}
132
148
  `
133
149
  : ''}
134
150
  ${this.captureState === 'error'
135
151
  ? html `
136
- <button class="btn btn-primary" data-simface-action="retry" @click=${this.beginCapture}>Try again</button>
137
- <button class="btn btn-ghost" data-simface-action="cancel" @click=${this.handleCancel}>Cancel</button>
152
+ <button class="btn btn-primary" data-simface-action="retry" @click=${this.beginCapture}>${this.retryLabel}</button>
138
153
  `
139
154
  : ''}
140
155
  </div>
@@ -150,12 +165,15 @@ let SimFaceCapture = class SimFaceCapture extends LitElement {
150
165
  this.feedbackMessage = 'Requesting camera access...';
151
166
  this.feedbackTone = 'neutral';
152
167
  const options = normalizeCaptureOptions({
153
- presentation: 'embedded',
154
168
  capturePreference: this.capturePreference,
155
169
  allowMediaPickerFallback: this.allowMediaPickerFallback,
156
- label: this.label,
157
- confirmLabel: this.confirmLabel,
158
- });
170
+ }, this);
171
+ options.label = this.label;
172
+ options.idleFeedbackLabel = this.idleFeedbackLabel;
173
+ options.confirmLabel = this.confirmLabel;
174
+ options.captureLabel = this.captureLabel;
175
+ options.retakeLabel = this.retakeLabel;
176
+ options.retryLabel = this.retryLabel;
159
177
  const capabilities = await resolveCaptureCapabilities({
160
178
  capturePreference: options.capturePreference,
161
179
  });
@@ -321,7 +339,7 @@ let SimFaceCapture = class SimFaceCapture extends LitElement {
321
339
  this.clearPreviewUrl();
322
340
  this.captureState = 'idle';
323
341
  this.errorMessage = '';
324
- this.feedbackMessage = 'Start a capture to see camera guidance here.';
342
+ this.feedbackMessage = this.idleFeedbackLabel;
325
343
  this.feedbackTone = 'neutral';
326
344
  this.syncProgress(0);
327
345
  this.qualityResult = null;
@@ -396,25 +414,56 @@ SimFaceCapture.styles = css `
396
414
  :host([embedded]) {
397
415
  max-width: none;
398
416
  margin: 0;
399
- text-align: left;
400
417
  }
401
418
 
402
419
  .container {
420
+ position: relative;
403
421
  padding: 16px;
404
422
  border: 1px solid #e0e0e0;
405
- border-radius: 12px;
423
+ border-radius: 16px;
406
424
  background: #fafafa;
407
425
  }
408
426
 
427
+ .close-btn {
428
+ position: absolute;
429
+ top: 12px;
430
+ left: 12px;
431
+ z-index: 10;
432
+ display: inline-flex;
433
+ align-items: center;
434
+ justify-content: center;
435
+ width: 40px;
436
+ height: 40px;
437
+ padding: 0;
438
+ border: none;
439
+ border-radius: 50%;
440
+ background: rgba(0, 0, 0, 0.06);
441
+ color: #49454f;
442
+ cursor: pointer;
443
+ transition: background-color 0.15s;
444
+ }
445
+
446
+ .close-btn:hover {
447
+ background: rgba(0, 0, 0, 0.12);
448
+ }
449
+
450
+ .close-btn svg {
451
+ width: 20px;
452
+ height: 20px;
453
+ }
454
+
409
455
  .capture-shell {
410
456
  display: flex;
411
457
  flex-direction: column;
458
+ align-items: center;
412
459
  gap: 16px;
413
460
  }
414
461
 
415
462
  .capture-copy {
416
463
  margin: 0;
417
464
  color: #334155;
465
+ text-align: center;
466
+ width: 100%;
418
467
  }
419
468
 
420
469
  .stage {
@@ -427,7 +476,6 @@ SimFaceCapture.styles = css `
427
476
  radial-gradient(circle at top, rgba(56, 189, 248, 0.16), transparent 30%),
428
477
  linear-gradient(180deg, #0f172a, #020617);
429
478
  box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.2);
430
- align-self: center;
431
479
  }
432
480
 
433
481
  .video,
@@ -483,22 +531,23 @@ SimFaceCapture.styles = css `
483
531
 
484
532
  .btn-row {
485
533
  display: flex;
486
- flex-wrap: wrap;
487
534
  gap: 12px;
535
+ width: min(100%, 420px);
488
536
  }
489
537
 
490
538
  .btn {
491
539
  display: inline-flex;
492
540
  align-items: center;
493
541
  justify-content: center;
494
- padding: 12px 24px;
495
- margin: 8px 4px 0 0;
542
+ flex: 1;
543
+ padding: 14px 24px;
496
544
  border: none;
497
- border-radius: 999px;
498
- font-size: 16px;
545
+ border-radius: 100px;
546
+ font-size: 15px;
499
547
  font-weight: 600;
548
+ letter-spacing: 0.02em;
500
549
  cursor: pointer;
501
- transition: background-color 0.2s;
550
+ transition: background-color 0.15s, box-shadow 0.15s;
502
551
  }
503
552
 
504
553
  .btn-primary {
@@ -508,11 +557,33 @@ SimFaceCapture.styles = css `
508
557
 
509
558
  .btn-primary:hover {
510
559
  background: #1d4ed8;
560
+ box-shadow: 0 1px 3px rgba(37, 99, 235, 0.3);
511
561
  }
512
562
 
513
563
  .btn-primary:disabled {
514
564
  background: #93c5fd;
515
565
  cursor: not-allowed;
566
+ box-shadow: none;
567
+ }
568
+
569
+ .btn-confirm {
570
+ background: #16a34a;
571
+ color: white;
572
+ }
573
+
574
+ .btn-confirm:hover {
575
+ background: #15803d;
576
+ box-shadow: 0 1px 3px rgba(22, 163, 74, 0.3);
577
+ }
578
+
579
+ .btn-retake {
580
+ background: #dc2626;
581
+ color: white;
582
+ }
583
+
584
+ .btn-retake:hover {
585
+ background: #b91c1c;
586
+ box-shadow: 0 1px 3px rgba(220, 38, 38, 0.3);
516
587
  }
517
588
 
518
589
  .btn-secondary {
@@ -524,17 +595,17 @@ SimFaceCapture.styles = css `
524
595
  background: #d1d5db;
525
596
  }
526
597
 
527
- .btn-ghost {
528
- background: #e2e8f0;
529
- color: #0f172a;
530
- }
531
-
532
598
  .quality-msg {
533
599
  padding: 10px 14px;
534
600
  border-radius: 14px;
535
- margin: 8px 0 0;
536
601
  font-size: 14px;
537
602
  font-weight: 600;
603
+ width: min(100%, 420px);
604
+ min-height: 44px;
605
+ display: flex;
606
+ align-items: center;
607
+ justify-content: center;
608
+ text-align: center;
538
609
  }
539
610
 
540
611
  .quality-good {
@@ -581,6 +652,9 @@ SimFaceCapture.styles = css `
581
652
  __decorate([
582
653
  property({ type: String })
583
654
  ], SimFaceCapture.prototype, "label", void 0);
655
+ __decorate([
656
+ property({ type: String, attribute: 'idle-feedback-label' })
657
+ ], SimFaceCapture.prototype, "idleFeedbackLabel", void 0);
584
658
  __decorate([
585
659
  property({ type: Boolean, reflect: true })
586
660
  ], SimFaceCapture.prototype, "embedded", void 0);
@@ -590,6 +664,15 @@ __decorate([
590
664
  __decorate([
591
665
  property({ type: String, attribute: 'confirm-label' })
592
666
  ], SimFaceCapture.prototype, "confirmLabel", void 0);
667
+ __decorate([
668
+ property({ type: String, attribute: 'capture-label' })
669
+ ], SimFaceCapture.prototype, "captureLabel", void 0);
670
+ __decorate([
671
+ property({ type: String, attribute: 'retake-label' })
672
+ ], SimFaceCapture.prototype, "retakeLabel", void 0);
673
+ __decorate([
674
+ property({ type: String, attribute: 'retry-label' })
675
+ ], SimFaceCapture.prototype, "retryLabel", void 0);
593
676
  __decorate([
594
677
  property({ type: String, attribute: 'capture-preference' })
595
678
  ], SimFaceCapture.prototype, "capturePreference", void 0);