@authhero/widget 0.32.42 → 0.33.0

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/hydrate/index.mjs CHANGED
@@ -6956,6 +6956,94 @@ class AuthheroWidget {
6956
6956
  }
6957
6957
  });
6958
6958
  }
6959
+ /**
6960
+ * Pending resolvers awaiting the next render, flushed by
6961
+ * `componentDidRender`. Lets `swapScreen` wait until the new screen is
6962
+ * actually in the DOM before measuring its height.
6963
+ */
6964
+ pendingRenderResolvers = [];
6965
+ componentDidRender() {
6966
+ if (this.pendingRenderResolvers.length === 0)
6967
+ return;
6968
+ const resolvers = this.pendingRenderResolvers;
6969
+ this.pendingRenderResolvers = [];
6970
+ resolvers.forEach((resolve) => resolve());
6971
+ }
6972
+ /**
6973
+ * Resolves after Stencil flushes the next render, so `swapScreen` waits for
6974
+ * the new screen to be in the DOM before measuring its height.
6975
+ */
6976
+ nextRender() {
6977
+ return new Promise((resolve) => {
6978
+ let settled = false;
6979
+ const done = () => {
6980
+ if (settled)
6981
+ return;
6982
+ settled = true;
6983
+ resolve();
6984
+ };
6985
+ this.pendingRenderResolvers.push(done);
6986
+ // Safety net: a screen swap always triggers a render here, but never
6987
+ // hang the swap if one somehow doesn't fire — resolve after a couple of
6988
+ // frames at the latest.
6989
+ requestAnimationFrame(() => requestAnimationFrame(done));
6990
+ });
6991
+ }
6992
+ /**
6993
+ * Apply a screen swap, animating the widget card's height to the next
6994
+ * screen's size (Stripe-style): lock the current height, swap the content,
6995
+ * measure the new natural height, then animate between them. The content
6996
+ * itself doesn't fade — the card just resizes.
6997
+ *
6998
+ * The card (.widget-container) is animated rather than the host because it
6999
+ * carries the background, so its resize is visible; clipping its content
7000
+ * (not the host) also keeps the card's drop-shadow intact.
7001
+ *
7002
+ * Degrades to an immediate swap when the Web Animations API is unavailable
7003
+ * or the user prefers reduced motion.
7004
+ */
7005
+ async swapScreen(apply) {
7006
+ const card = this.el.shadowRoot?.querySelector(".widget-container");
7007
+ const prefersReducedMotion = typeof window.matchMedia === "function" &&
7008
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
7009
+ if (!card || typeof card.animate !== "function" || prefersReducedMotion) {
7010
+ apply();
7011
+ return;
7012
+ }
7013
+ // Lock the current height and clip, so swapping the content doesn't jump
7014
+ // before the animation runs.
7015
+ const startHeight = card.getBoundingClientRect().height;
7016
+ card.style.height = `${startHeight}px`;
7017
+ card.style.overflow = "hidden";
7018
+ apply();
7019
+ // Wait for the new screen to render before measuring its height.
7020
+ await this.nextRender();
7021
+ const cleanup = () => {
7022
+ card.style.height = "";
7023
+ card.style.overflow = "";
7024
+ };
7025
+ // Measure the new content's natural height without painting: toggle to
7026
+ // auto, read, restore — all synchronous, so there's no visible jump.
7027
+ card.style.height = "auto";
7028
+ const endHeight = card.getBoundingClientRect().height;
7029
+ card.style.height = `${startHeight}px`;
7030
+ card.getBoundingClientRect(); // force reflow from startHeight
7031
+ if (Math.abs(endHeight - startHeight) < 1) {
7032
+ cleanup();
7033
+ return;
7034
+ }
7035
+ const animation = card.animate([{ height: `${startHeight}px` }, { height: `${endHeight}px` }], { duration: 520, easing: "cubic-bezier(0.22, 1, 0.36, 1)" });
7036
+ try {
7037
+ await animation.finished;
7038
+ }
7039
+ catch {
7040
+ // Animation cancelled (e.g. superseded by a newer swap); the final
7041
+ // DOM state is already correct, so just fall through to cleanup.
7042
+ }
7043
+ finally {
7044
+ cleanup();
7045
+ }
7046
+ }
6959
7047
  /**
6960
7048
  * Get the effective autoNavigate value (defaults to autoSubmit if not set)
6961
7049
  */
@@ -7258,45 +7346,55 @@ class AuthheroWidget {
7258
7346
  }
7259
7347
  else if (!response.ok && result.screen) {
7260
7348
  // Handle validation errors (400 response) — preserve user input
7261
- this._screen = result.screen;
7262
- this.initFormDataFromDefaults(result.screen);
7263
- this.screenChange.emit(result.screen);
7264
- this.updateDataScreenAttribute();
7265
- this.focusFirstInput();
7349
+ await this.swapScreen(() => {
7350
+ // Clear loading first so the captured "after" snapshot doesn't
7351
+ // freeze a disabled button into the cross-fade.
7352
+ this.loading = false;
7353
+ this._screen = result.screen;
7354
+ this.initFormDataFromDefaults(result.screen);
7355
+ this.screenChange.emit(result.screen);
7356
+ this.updateDataScreenAttribute();
7357
+ this.focusFirstInput();
7358
+ });
7266
7359
  }
7267
7360
  else if (result.screen) {
7268
7361
  // Next screen (success)
7269
- this._screen = result.screen;
7270
- this.formData = {};
7271
- this.initFormDataFromDefaults(result.screen);
7272
- this.screenChange.emit(result.screen);
7273
- this.updateDataScreenAttribute();
7274
- // Update screenId if returned in response
7275
- if (result.screenId) {
7276
- this.screenId = result.screenId;
7277
- }
7278
- // Persist state (especially for session storage mode)
7279
- this.persistState();
7280
- // Update URL path if navigateUrl is provided (client-side navigation)
7281
- if (result.navigateUrl && this.shouldAutoNavigate) {
7282
- window.history.pushState({ screen: result.screenId, state: this.state }, "", result.navigateUrl);
7283
- }
7284
- // Apply branding if included
7285
- if (result.branding) {
7286
- this._branding = result.branding;
7287
- this.applyThemeStyles();
7288
- }
7289
- // Update state if returned
7290
- if (result.state) {
7291
- this.state = result.state;
7362
+ await this.swapScreen(() => {
7363
+ // Clear loading first so the captured "after" snapshot doesn't
7364
+ // freeze a disabled button into the cross-fade.
7365
+ this.loading = false;
7366
+ this._screen = result.screen;
7367
+ this.formData = {};
7368
+ this.initFormDataFromDefaults(result.screen);
7369
+ this.screenChange.emit(result.screen);
7370
+ this.updateDataScreenAttribute();
7371
+ // Update screenId if returned in response
7372
+ if (result.screenId) {
7373
+ this.screenId = result.screenId;
7374
+ }
7375
+ // Persist state (especially for session storage mode)
7292
7376
  this.persistState();
7293
- }
7294
- // Perform WebAuthn ceremony if present (structured data, not script)
7295
- if (result.ceremony) {
7296
- this.performWebAuthnCeremony(result.ceremony);
7297
- }
7298
- // Focus first input on new screen
7299
- this.focusFirstInput();
7377
+ // Update URL path if navigateUrl is provided (client-side navigation)
7378
+ if (result.navigateUrl && this.shouldAutoNavigate) {
7379
+ window.history.pushState({ screen: result.screenId, state: this.state }, "", result.navigateUrl);
7380
+ }
7381
+ // Apply branding if included
7382
+ if (result.branding) {
7383
+ this._branding = result.branding;
7384
+ this.applyThemeStyles();
7385
+ }
7386
+ // Update state if returned
7387
+ if (result.state) {
7388
+ this.state = result.state;
7389
+ this.persistState();
7390
+ }
7391
+ // Perform WebAuthn ceremony if present (structured data, not script)
7392
+ if (result.ceremony) {
7393
+ this.performWebAuthnCeremony(result.ceremony);
7394
+ }
7395
+ // Focus first input on new screen
7396
+ this.focusFirstInput();
7397
+ });
7300
7398
  }
7301
7399
  else if (result.complete) {
7302
7400
  // Flow complete without redirect
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authhero/widget",
3
- "version": "0.32.42",
3
+ "version": "0.33.0",
4
4
  "description": "Server-Driven UI widget for AuthHero authentication flows",
5
5
  "repository": {
6
6
  "type": "git",