@tn3w/openage 1.0.5 → 1.0.6

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
@@ -16,11 +16,11 @@ button binding.
16
16
 
17
17
  ## At a Glance
18
18
 
19
- | Browser-side | Server-backed | UI |
20
- | ----------------------- | -------------------------- | -------------------------- |
21
- | On-device face analysis | Optional WASM verification | Embedded widget + popup |
22
- | No raw camera upload | Signed sessions and tokens | Normal, compact, invisible |
23
- | Serverless soft gates | Hosted or custom backend | Auto, light, dark |
19
+ | Browser-side | Server-backed | UI |
20
+ | ----------------------- | -------------------------- | -------------------------------- |
21
+ | On-device face analysis | Optional WASM verification | Widget popup, inline embed, bind |
22
+ | No raw camera upload | Signed sessions and tokens | Normal, compact, invisible |
23
+ | Serverless soft gates | Hosted or custom backend | Auto, light, dark |
24
24
 
25
25
  ## Install
26
26
 
@@ -54,12 +54,27 @@ import OpenAge from '@tn3w/openage';
54
54
 
55
55
  OpenAge.render('#gate', {
56
56
  mode: 'serverless',
57
+ layout: 'inline',
57
58
  minAge: 18,
58
59
  callback: (token) => console.log(token),
59
60
  errorCallback: (error) => console.error(error),
60
61
  });
61
62
  ```
62
63
 
64
+ ### Inline Embed
65
+
66
+ ```js
67
+ OpenAge.render('#gate', {
68
+ mode: 'serverless',
69
+ layout: 'inline',
70
+ minAge: 18,
71
+ });
72
+ ```
73
+
74
+ `layout: 'inline'` removes the checkbox shell and renders the verification
75
+ panel directly in the container. The first verification step starts
76
+ immediately after loading.
77
+
63
78
  ### Bound Flow
64
79
 
65
80
  ```js
@@ -104,6 +119,7 @@ the popup automatically after 5 seconds.
104
119
  | Param | Values |
105
120
  | --------- | --------------------------------- |
106
121
  | `mode` | `serverless`, `sitekey`, `custom` |
122
+ | `layout` | `widget`, `inline` |
107
123
  | `theme` | `light`, `dark`, `auto` |
108
124
  | `size` | `normal`, `compact`, `invisible` |
109
125
  | `minAge` | number, default `18` |
@@ -122,7 +138,7 @@ python server.py
122
138
  ```
123
139
 
124
140
  The repository also includes `demo/`, a minimal GitHub Pages build that loads
125
- the jsDelivr bundle for `@tn3w/openage` in embedded `serverless` mode.
141
+ the jsDelivr bundle for `@tn3w/openage` in inline embedded `serverless` mode.
126
142
 
127
143
  ## Development
128
144
 
@@ -541,6 +541,20 @@ const STYLES = `
541
541
  flex-direction: column;
542
542
  }
543
543
 
544
+ .oa-inline-shell {
545
+ background: var(--oa-bg);
546
+ border: 1px solid var(--oa-border);
547
+ border-radius: var(--oa-radius);
548
+ box-shadow: 0 20px 60px rgba(0,0,0,0.18),
549
+ 0 0 0 1px rgba(0,0,0,0.06);
550
+ width: min(100%, 360px);
551
+ max-width: 100%;
552
+ overflow: hidden;
553
+ display: flex;
554
+ flex-direction: column;
555
+ margin: 0 auto;
556
+ }
557
+
544
558
  .oa-modal-overlay {
545
559
  position: fixed;
546
560
  inset: 0;
@@ -1108,8 +1122,7 @@ const MEDIAPIPE_MODEL =
1108
1122
  'face_landmarker/face_landmarker/float16/1/' +
1109
1123
  'face_landmarker.task';
1110
1124
 
1111
- const FACEAPI_CDN =
1112
- 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js';
1125
+ const FACEAPI_CDN = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js';
1113
1126
 
1114
1127
  const FACEAPI_MODEL_CDN =
1115
1128
  'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@master/weights';
@@ -1146,6 +1159,7 @@ class Widget {
1146
1159
  this.popup = null;
1147
1160
  this.shadow = null;
1148
1161
  this.elements = {};
1162
+ this.popupElements = null;
1149
1163
  this.onChallenge = null;
1150
1164
  this.onStartClick = null;
1151
1165
  this.popupFrame = 0;
@@ -1158,6 +1172,7 @@ class Widget {
1158
1172
  const host = document.createElement('div');
1159
1173
  host.id = this.id;
1160
1174
  this.shadow = host.attachShadow({ mode: 'open' });
1175
+ this.host = host;
1161
1176
 
1162
1177
  const style = document.createElement('style');
1163
1178
  style.textContent = STYLES;
@@ -1170,7 +1185,12 @@ class Widget {
1170
1185
  if (this.params.size === 'invisible') {
1171
1186
  host.style.display = 'none';
1172
1187
  this.container.appendChild(host);
1173
- this.host = host;
1188
+ return;
1189
+ }
1190
+
1191
+ if (this.isInlineLayout()) {
1192
+ this.renderInlineShell();
1193
+ this.container.appendChild(host);
1174
1194
  return;
1175
1195
  }
1176
1196
 
@@ -1206,7 +1226,32 @@ class Widget {
1206
1226
  this.elements.errorSlot = this.shadow.querySelector('.oa-error-slot');
1207
1227
 
1208
1228
  this.container.appendChild(host);
1209
- this.host = host;
1229
+ }
1230
+
1231
+ isInlineLayout() {
1232
+ return this.params.layout === 'inline';
1233
+ }
1234
+
1235
+ renderInlineShell() {
1236
+ const inlineShell = document.createElement('div');
1237
+ inlineShell.className = 'oa-inline-shell';
1238
+ inlineShell.innerHTML = this.buildPopupContent({ closeable: false });
1239
+ this.shadow.appendChild(inlineShell);
1240
+
1241
+ this.popup = {
1242
+ host: this.host,
1243
+ root: inlineShell,
1244
+ inline: true,
1245
+ };
1246
+
1247
+ this.bindPopupEvents(inlineShell);
1248
+ }
1249
+
1250
+ resetInlineShell() {
1251
+ if (!this.popup?.root) return;
1252
+
1253
+ this.popup.root.innerHTML = this.buildPopupContent({ closeable: false });
1254
+ this.bindPopupEvents(this.popup.root);
1210
1255
  }
1211
1256
 
1212
1257
  startChallenge() {
@@ -1239,6 +1284,14 @@ class Widget {
1239
1284
  }
1240
1285
 
1241
1286
  openPopup() {
1287
+ if (this.isInlineLayout()) {
1288
+ if (!this.popup) {
1289
+ this.renderInlineShell();
1290
+ }
1291
+
1292
+ return this.getVideo();
1293
+ }
1294
+
1242
1295
  if (this.popup) return this.getVideo();
1243
1296
 
1244
1297
  const anchor = this.getPopupAnchor();
@@ -1416,7 +1469,7 @@ class Widget {
1416
1469
  this.popup.root.style.pointerEvents = 'auto';
1417
1470
  }
1418
1471
 
1419
- buildPopupContent() {
1472
+ buildPopupContent({ closeable = true } = {}) {
1420
1473
  return `
1421
1474
  <div class="oa-header">
1422
1475
  <div class="oa-title">
@@ -1427,10 +1480,14 @@ class Widget {
1427
1480
  </a>
1428
1481
  <span class="oa-badge">on-device</span>
1429
1482
  </div>
1430
- <button class="oa-close-btn"
1431
- aria-label="Close">
1432
- ${CLOSE_SVG}
1433
- </button>
1483
+ ${
1484
+ closeable
1485
+ ? `<button class="oa-close-btn"
1486
+ aria-label="Close">
1487
+ ${CLOSE_SVG}
1488
+ </button>`
1489
+ : ''
1490
+ }
1434
1491
  </div>
1435
1492
  <div class="oa-body">
1436
1493
  ${heroTemplate('Initializing…')}
@@ -1443,7 +1500,7 @@ class Widget {
1443
1500
  `;
1444
1501
  }
1445
1502
 
1446
- bindPopupEvents(root, shadow) {
1503
+ bindPopupEvents(root) {
1447
1504
  const closeBtn = root.querySelector('.oa-close-btn');
1448
1505
  if (closeBtn) {
1449
1506
  closeBtn.addEventListener('click', () => {
@@ -1561,6 +1618,22 @@ class Widget {
1561
1618
  }
1562
1619
 
1563
1620
  showResult(outcome, message) {
1621
+ if (this.isInlineLayout()) {
1622
+ if (!this.popupElements?.body) return;
1623
+
1624
+ this.popupElements.body.innerHTML = resultTemplate(outcome, message);
1625
+ this.hideActions();
1626
+
1627
+ if (outcome === 'pass') {
1628
+ this.setState('verified');
1629
+ return;
1630
+ }
1631
+
1632
+ this.setState(outcome === 'fail' ? 'failed' : 'retry');
1633
+ this.showActions('Try Again');
1634
+ return;
1635
+ }
1636
+
1564
1637
  if (outcome === 'pass') {
1565
1638
  this.closePopup();
1566
1639
  this.setState('verified');
@@ -1600,8 +1673,9 @@ class Widget {
1600
1673
  if (!this.popupElements?.body) return;
1601
1674
 
1602
1675
  this.popupElements.body.innerHTML = errorStepTemplate(message);
1603
- this.popupElements.errorCountdown =
1604
- this.popupElements.body.querySelector('.oa-error-step-countdown');
1676
+ this.popupElements.errorCountdown = this.popupElements.body.querySelector(
1677
+ '.oa-error-step-countdown'
1678
+ );
1605
1679
  this.hideActions();
1606
1680
  }
1607
1681
 
@@ -1620,6 +1694,22 @@ class Widget {
1620
1694
 
1621
1695
  closePopup() {
1622
1696
  if (!this.popup) return;
1697
+
1698
+ if (this.popup.inline) {
1699
+ this.popup.cleanup?.();
1700
+ if (this.popupFrame) {
1701
+ cancelAnimationFrame(this.popupFrame);
1702
+ this.popupFrame = 0;
1703
+ }
1704
+ this.resetInlineShell();
1705
+
1706
+ if (this.state === 'loading') {
1707
+ this.setState('idle');
1708
+ }
1709
+
1710
+ return;
1711
+ }
1712
+
1623
1713
  this.popup.cleanup?.();
1624
1714
  this.popup.themeCleanup?.();
1625
1715
  this.popup.host.remove();
@@ -2819,9 +2909,18 @@ function resolveChallengeErrorMessage(error) {
2819
2909
  return 'Verification failed. Please try again.';
2820
2910
  }
2821
2911
 
2912
+ function isInlineLayout(widget) {
2913
+ return widget?.params?.layout === 'inline';
2914
+ }
2915
+
2822
2916
  async function showErrorStep(widget, error) {
2823
2917
  const message = resolveChallengeErrorMessage(error);
2824
2918
 
2919
+ if (isInlineLayout(widget)) {
2920
+ widget.showResult?.('retry', message);
2921
+ return;
2922
+ }
2923
+
2825
2924
  if (!widget.popup) {
2826
2925
  widget.openPopup?.();
2827
2926
  }
@@ -2874,10 +2973,14 @@ async function runServerless(widget, emitter) {
2874
2973
  await Promise.all([loadVision(), initAgeEstimator()]);
2875
2974
 
2876
2975
  modelBuffer = await loadModel();
2877
- widget.showReady();
2878
2976
 
2879
- await waitForStart(widget);
2880
- await startCameraFlow(widget, modelBuffer);
2977
+ if (isInlineLayout(widget)) {
2978
+ await startCameraFlow(widget, modelBuffer);
2979
+ } else {
2980
+ widget.showReady();
2981
+ await waitForStart(widget);
2982
+ await startCameraFlow(widget, modelBuffer);
2983
+ }
2881
2984
 
2882
2985
  const transport = createTransport('serverless', params);
2883
2986
 
@@ -2966,10 +3069,16 @@ async function runServer(widget, emitter) {
2966
3069
  captureFrame: () => (captureFrame() ? 'true' : 'null'),
2967
3070
  });
2968
3071
 
2969
- widget.showReady();
2970
- await waitForStart(widget);
3072
+ let video;
3073
+
3074
+ if (isInlineLayout(widget)) {
3075
+ video = widget.showCamera();
3076
+ } else {
3077
+ widget.showReady();
3078
+ await waitForStart(widget);
3079
+ video = widget.showCamera();
3080
+ }
2971
3081
 
2972
- const video = widget.showCamera();
2973
3082
  widget.setVideoStatus('Requesting camera…');
2974
3083
  await startCamera(video);
2975
3084
  exposeMirrorVideo(video);
@@ -3272,8 +3381,19 @@ class EventEmitter {
3272
3381
  const emitter = new EventEmitter();
3273
3382
  const widgets = new Map();
3274
3383
 
3384
+ function normalizeLayout(layout) {
3385
+ if (layout === 'inline' || layout === 'embed' || layout === 'embedded') {
3386
+ return 'inline';
3387
+ }
3388
+
3389
+ return 'widget';
3390
+ }
3391
+
3275
3392
  function normalizeParams(params) {
3276
3393
  const globalConfig = typeof window !== 'undefined' ? window.openage || {} : {};
3394
+ const layout = normalizeLayout(
3395
+ params.layout ?? params.display ?? globalConfig.layout ?? globalConfig.display
3396
+ );
3277
3397
 
3278
3398
  return {
3279
3399
  mode: 'serverless',
@@ -3282,6 +3402,7 @@ function normalizeParams(params) {
3282
3402
  minAge: 18,
3283
3403
  ...globalConfig,
3284
3404
  ...params,
3405
+ layout,
3285
3406
  };
3286
3407
  }
3287
3408
 
@@ -3290,6 +3411,10 @@ function startWidget(widget) {
3290
3411
  emitter.emit('opened', widget.id);
3291
3412
  runChallenge(widget, emitter);
3292
3413
  };
3414
+
3415
+ if (widget.params.layout === 'inline') {
3416
+ widget.startChallenge();
3417
+ }
3293
3418
  }
3294
3419
 
3295
3420
  function render(container, params = {}) {
@@ -3438,6 +3563,7 @@ function autoRender() {
3438
3563
  size: element.dataset.size,
3439
3564
  action: element.dataset.action,
3440
3565
  mode: element.dataset.mode,
3566
+ layout: element.dataset.layout || element.dataset.display,
3441
3567
  server: element.dataset.server,
3442
3568
  };
3443
3569