@authhero/widget 0.40.2 → 0.41.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.
@@ -515,7 +515,7 @@ function escapeAttr(value) {
515
515
  .replace(/>/g, ">");
516
516
  }
517
517
 
518
- const authheroWidgetCss = () => `:host{display:block;font-family:var( --ah-font-family, "ulp-font", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, sans-serif );font-size:var(--ah-font-size-base, 16px);line-height:var(--ah-line-height-base, 1.5);color:var(--ah-color-text, #1e212a);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.widget-container{width:var(--ah-widget-max-width, 400px);margin:0 auto;background-color:var(--ah-color-bg, #ffffff);border-radius:var(--ah-widget-radius, 5px);border:var(--ah-widget-border-width, 0) solid var(--ah-widget-border-color, transparent);box-shadow:var(--ah-widget-shadow, 0 4px 22px 0 rgba(0, 0, 0, 0.11));box-sizing:border-box}.widget-header{padding:var(--ah-header-padding, 40px 48px 24px)}.widget-body{padding:var(--ah-body-padding, 0 48px 40px)}.logo-wrapper{display:var(--ah-logo-display, flex);justify-content:var(--ah-logo-align, center);margin-bottom:8px}.logo{display:block;height:var(--ah-logo-height, 52px);max-width:100%;width:auto;object-fit:contain}.title{font-size:var(--ah-font-size-title, 24px);font-weight:var(--ah-font-weight-title, 700);text-align:var(--ah-title-align, center);margin:var(--ah-title-margin, 24px 0 24px);color:var(--ah-color-text-header, var(--ah-color-header, #1e212a));line-height:1.2}.description{font-size:var(--ah-font-size-subtitle, 16px);font-weight:var(--ah-font-weight-subtitle, 400);text-align:var(--ah-title-align, center);margin:var(--ah-description-margin, 0 0 8px);color:var(--ah-color-text, #1e212a);line-height:1.5}.message{padding:12px 16px;border-radius:4px;margin-bottom:16px;font-size:14px;line-height:1.5}.message-error{background-color:var(--ah-color-error-bg, #ffeaea);color:var(--ah-color-error, #d03c38);border-left:3px solid var(--ah-color-error, #d03c38)}.message-success{background-color:var(--ah-color-success-bg, #e6f9f1);color:var(--ah-color-success, #13a769);border-left:3px solid var(--ah-color-success, #13a769)}form{display:flex;flex-direction:column}.form-content{display:flex;flex-direction:column}.social-section{display:flex;flex-direction:column;gap:8px;order:var(--ah-social-order, 2)}.fields-section{display:flex;flex-direction:column;order:var(--ah-fields-order, 0)}.fields-list{display:flex;flex-direction:column}.fields-list-scroll{max-height:var(--ah-choice-list-max-height, 320px);overflow-y:auto;gap:var(--ah-choice-list-gap, 8px);padding-right:4px}.choice-list-search{width:100%;padding:16px;margin-bottom:12px;font-size:16px;font-family:inherit;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:var(--ah-input-radius, 3px);outline:none;box-sizing:border-box;transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out}.choice-list-search:hover{border-color:var(--ah-color-border-hover, #65676e)}.choice-list-search:focus{border-color:var(--ah-color-primary, var(--ah-color-link, #635dff))}.choice-list-empty{padding:16px;text-align:center;font-size:14px;color:var(--ah-color-text-muted, #65676e)}.divider{display:flex;align-items:center;text-align:center;margin:16px 0;order:var(--ah-divider-order, 1)}.divider::before,.divider::after{content:"";flex:1;border-bottom:1px solid var(--ah-color-border-muted, #c9cace)}.divider-text{padding:0 10px;font-size:12px;font-weight:400;color:var(--ah-color-text-muted, #65676e);text-transform:uppercase;letter-spacing:0}.links{display:flex;flex-direction:column;align-items:center;gap:8px;margin-top:16px}.link-wrapper{font-size:var(--ah-font-size-body, 14px);font-weight:var(--ah-font-weight-body, 400);color:var(--ah-color-text, #1e212a)}.link{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:var(--ah-font-size-link, 14px);font-weight:var(--ah-font-weight-link, 400);transition:color 150ms ease}.link:hover{text-decoration:underline}.link:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-link, #635dff));outline-offset:2px;border-radius:2px}.widget-footer{margin-top:16px;text-align:center;font-size:12px;color:var(--ah-color-text-muted, #65676e)}.widget-footer a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:12px;transition:color 150ms ease}.widget-footer a:hover{text-decoration:underline}.widget-footer a:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-link, #635dff));outline-offset:2px;border-radius:2px}.loading-spinner{width:32px;height:32px;margin:24px auto;border:3px solid var(--ah-color-border-muted, #e0e1e3);border-top-color:var(--ah-color-primary, #635dff);border-radius:50%;animation:spin 0.8s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.error-message{text-align:center;color:var(--ah-color-error, #d03c38);padding:16px;font-size:14px}@media (max-width: 480px){:host(:not([floating])){display:block;width:100%;min-height:100vh;background-color:var(--ah-color-bg, #ffffff)}:host(:not([floating])) .widget-container{box-shadow:none;border-radius:0;border:0;width:100%;margin:0}:host([floating]) .widget-container{width:100%;margin:0}.widget-header{padding:24px 16px 16px}.widget-body{padding:0 16px 24px}}`;
518
+ const authheroWidgetCss = () => `:host{display:block;font-family:var( --ah-font-family, "ulp-font", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, sans-serif );font-size:var(--ah-font-size-base, 16px);line-height:var(--ah-line-height-base, 1.5);color:var(--ah-color-text, #1e212a);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.widget-container{width:var(--ah-widget-max-width, 400px);margin:0 auto;background-color:var(--ah-color-bg, #ffffff);border-radius:var(--ah-widget-radius, 5px);border:var(--ah-widget-border-width, 0) solid var(--ah-widget-border-color, transparent);box-shadow:var(--ah-widget-shadow, 0 4px 22px 0 rgba(0, 0, 0, 0.11));box-sizing:border-box}.widget-header{padding:var(--ah-header-padding, 40px 48px 24px)}.widget-body{padding:var(--ah-body-padding, 0 48px 40px)}.logo-wrapper{display:var(--ah-logo-display, flex);justify-content:var(--ah-logo-align, center);margin-bottom:8px}.logo{display:block;height:var(--ah-logo-height, 52px);max-width:100%;width:auto;object-fit:contain}.title{font-size:var(--ah-font-size-title, 24px);font-weight:var(--ah-font-weight-title, 700);text-align:var(--ah-title-align, center);margin:var(--ah-title-margin, 24px 0 24px);color:var(--ah-color-text-header, var(--ah-color-header, #1e212a));line-height:1.2}.description{font-size:var(--ah-font-size-subtitle, 16px);font-weight:var(--ah-font-weight-subtitle, 400);text-align:var(--ah-title-align, center);margin:var(--ah-description-margin, 0 0 8px);color:var(--ah-color-text, #1e212a);line-height:1.5}.message{padding:12px 16px;border-radius:4px;margin-bottom:16px;font-size:14px;line-height:1.5}.message-error{background-color:var(--ah-color-error-bg, #ffeaea);color:var(--ah-color-error, #d03c38);border-left:3px solid var(--ah-color-error, #d03c38)}.message-success{background-color:var(--ah-color-success-bg, #e6f9f1);color:var(--ah-color-success, #13a769);border-left:3px solid var(--ah-color-success, #13a769)}form{display:flex;flex-direction:column}.form-footer,.secondary-actions,.prompt-partial{display:flex;flex-direction:column}.prompt-partial .ulp-field{display:flex;flex-direction:column;gap:6px;position:relative;margin-top:8px;margin-bottom:16px}.prompt-partial label{font-size:var(--ah-font-size-label, 14px);font-weight:var(--ah-font-weight-label, 400);color:var(--ah-color-text, #1e212a)}.prompt-partial .ulp-error input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),.prompt-partial .ulp-error select,.prompt-partial .ulp-error textarea{border-color:var(--ah-color-error, #d03c38)}.prompt-partial .ulp-error input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):focus,.prompt-partial .ulp-error select:focus,.prompt-partial .ulp-error textarea:focus{box-shadow:inset 0 0 0 1px var(--ah-color-error, #d03c38)}.prompt-partial .ulp-error-info{font-size:12px;color:var(--ah-color-error, #d03c38);margin-top:-2px;line-height:1.4}.prompt-partial .required{color:var(--ah-color-error, #d03c38);margin-left:2px}.prompt-partial .ulp-field>input+label,.prompt-partial .ulp-field>select+label,.prompt-partial .ulp-field>textarea+label{position:absolute;left:16px;top:50%;z-index:1;padding:0;background-color:transparent;color:var(--ah-color-text-muted, #65676e);pointer-events:none;transform:translateY(-50%);transition:all 0.15s ease-out}.prompt-partial .ulp-field>input:focus+label,.prompt-partial .ulp-field>input:not(:placeholder-shown)+label,.prompt-partial .ulp-field>select+label,.prompt-partial .ulp-field>textarea:focus+label,.prompt-partial .ulp-field>textarea:not(:placeholder-shown)+label{top:-8px;left:12px;padding:0 4px;background-color:var(--ah-color-bg, #ffffff);color:var(--ah-color-text-muted, #65676e);font-size:12px;transform:translateY(0)}.prompt-partial .ulp-field>input:focus+label,.prompt-partial .ulp-field>select:focus+label,.prompt-partial .ulp-field>textarea:focus+label{color:var(--ah-color-primary, #635dff)}.prompt-partial input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),.prompt-partial select,.prompt-partial textarea{width:100%;padding:16px;font-size:16px;font-family:inherit;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:var(--ah-input-radius, 3px);outline:none;box-sizing:border-box;transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out}.prompt-partial input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):hover,.prompt-partial select:hover,.prompt-partial textarea:hover{border-color:var(--ah-color-border-hover, #65676e)}.prompt-partial input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):focus,.prompt-partial select:focus,.prompt-partial textarea:focus{border-color:var(--ah-color-primary, #635dff);box-shadow:inset 0 0 0 1px var(--ah-color-primary, #635dff)}.form-content{display:flex;flex-direction:column}.social-section{display:flex;flex-direction:column;gap:8px;order:var(--ah-social-order, 2)}.fields-section{display:flex;flex-direction:column;order:var(--ah-fields-order, 0)}.fields-list{display:flex;flex-direction:column}.fields-list-scroll{max-height:var(--ah-choice-list-max-height, 320px);overflow-y:auto;gap:var(--ah-choice-list-gap, 8px);padding-right:4px}.choice-list-search{width:100%;padding:16px;margin-bottom:12px;font-size:16px;font-family:inherit;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:var(--ah-input-radius, 3px);outline:none;box-sizing:border-box;transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out}.choice-list-search:hover{border-color:var(--ah-color-border-hover, #65676e)}.choice-list-search:focus{border-color:var(--ah-color-primary, var(--ah-color-link, #635dff))}.choice-list-empty{padding:16px;text-align:center;font-size:14px;color:var(--ah-color-text-muted, #65676e)}.divider{display:flex;align-items:center;text-align:center;margin:16px 0;order:var(--ah-divider-order, 1)}.divider::before,.divider::after{content:"";flex:1;border-bottom:1px solid var(--ah-color-border-muted, #c9cace)}.divider-text{padding:0 10px;font-size:12px;font-weight:400;color:var(--ah-color-text-muted, #65676e);text-transform:uppercase;letter-spacing:0}.links{display:flex;flex-direction:column;align-items:center;gap:8px;margin-top:16px}.link-wrapper{font-size:var(--ah-font-size-body, 14px);font-weight:var(--ah-font-weight-body, 400);color:var(--ah-color-text, #1e212a)}.link{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:var(--ah-font-size-link, 14px);font-weight:var(--ah-font-weight-link, 400);transition:color 150ms ease}.link:hover{text-decoration:underline}.link:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-link, #635dff));outline-offset:2px;border-radius:2px}.widget-footer{margin-top:16px;text-align:center;font-size:12px;color:var(--ah-color-text-muted, #65676e)}.widget-footer a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:12px;transition:color 150ms ease}.widget-footer a:hover{text-decoration:underline}.widget-footer a:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-link, #635dff));outline-offset:2px;border-radius:2px}.loading-spinner{width:32px;height:32px;margin:24px auto;border:3px solid var(--ah-color-border-muted, #e0e1e3);border-top-color:var(--ah-color-primary, #635dff);border-radius:50%;animation:spin 0.8s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.error-message{text-align:center;color:var(--ah-color-error, #d03c38);padding:16px;font-size:14px}@media (max-width: 480px){:host(:not([floating])){display:block;width:100%;min-height:100vh;background-color:var(--ah-color-bg, #ffffff)}:host(:not([floating])) .widget-container{box-shadow:none;border-radius:0;border:0;width:100%;margin:0}:host([floating]) .widget-container{width:100%;margin:0}.widget-header{padding:24px 16px 16px}.widget-body{padding:0 16px 24px}}`;
519
519
 
520
520
  /**
521
521
  * When a screen's field section is made up entirely of choice buttons
@@ -530,6 +530,19 @@ const CHOICE_LIST_SEARCH_THRESHOLD = 5;
530
530
  * (CARDS, FILE, RECAPTCHA, …) can carry `required` in the schema too, and
531
531
  * gating on them would leave Continue disabled with no way to satisfy it.
532
532
  */
533
+ /**
534
+ * Field types rendered as a single plain text input whose value maps 1:1 to
535
+ * formData. Composite fields (TEL, DATE) and choice/checkbox fields keep
536
+ * internal state, so they're not adopted from pre-hydration markup.
537
+ */
538
+ const PREHYDRATION_TEXT_TYPES = new Set([
539
+ "TEXT",
540
+ "EMAIL",
541
+ "CODE",
542
+ "PASSWORD",
543
+ "NUMBER",
544
+ "URL",
545
+ ]);
533
546
  const FILLABLE_FIELD_TYPES = new Set([
534
547
  "TEXT",
535
548
  "EMAIL",
@@ -910,7 +923,24 @@ const AuthheroWidget = class {
910
923
  * actually in the DOM before measuring its height.
911
924
  */
912
925
  pendingRenderResolvers = [];
926
+ /**
927
+ * Number of `swapScreen` calls still running (rendering or animating the
928
+ * card height). A counter rather than a flag because a newer swap can start
929
+ * before a superseded one has settled.
930
+ */
931
+ activeSwaps = 0;
932
+ updateReadyAttribute() {
933
+ {
934
+ this.el.toggleAttribute("data-ready", !this.loading && this.activeSwaps === 0);
935
+ }
936
+ }
913
937
  componentDidRender() {
938
+ // `data-ready` marks the widget as interactive: hydrated in the browser
939
+ // (the SSR markup already carries Stencil's `hydrated` class, so that
940
+ // can't tell server and client apart) and not mid-submit or mid-swap.
941
+ // Stencil defers this hook until child nodes have rendered, so the
942
+ // fields are live too. E2E tests wait on `authhero-widget[data-ready]`.
943
+ this.updateReadyAttribute();
914
944
  if (this.pendingRenderResolvers.length === 0)
915
945
  return;
916
946
  const resolvers = this.pendingRenderResolvers;
@@ -958,6 +988,18 @@ const AuthheroWidget = class {
958
988
  apply();
959
989
  return;
960
990
  }
991
+ // Hold `data-ready` off until the new screen has rendered and the height
992
+ // animation has finished; `apply()` clears `loading` before either.
993
+ this.activeSwaps++;
994
+ try {
995
+ await this.animateSwap(card, apply);
996
+ }
997
+ finally {
998
+ this.activeSwaps--;
999
+ this.updateReadyAttribute();
1000
+ }
1001
+ }
1002
+ async animateSwap(card, apply) {
961
1003
  // Lock the current height and clip, so swapping the content doesn't jump
962
1004
  // before the animation runs.
963
1005
  const startHeight = card.getBoundingClientRect().height;
@@ -1122,6 +1164,9 @@ const AuthheroWidget = class {
1122
1164
  if (!this._authParams) {
1123
1165
  this.watchAuthParams(this.authParams ?? this.readJsonScript("auth-params"));
1124
1166
  }
1167
+ if (this._screen) {
1168
+ this.adoptPrehydrationValues(this._screen);
1169
+ }
1125
1170
  // Load persisted state if available
1126
1171
  this.loadPersistedState();
1127
1172
  // Fetch screen from API if URL provided and no screen prop
@@ -1129,6 +1174,41 @@ const AuthheroWidget = class {
1129
1174
  await this.fetchScreen(this.screenId);
1130
1175
  }
1131
1176
  }
1177
+ /**
1178
+ * Carry over text the user (or a password manager, or a test driver) put
1179
+ * into the server-rendered inputs before the client bundle loaded. Those
1180
+ * inputs had no `onInput` handler yet, so `formData` never saw the value:
1181
+ * the first client render would reset them to "" and keep the submit
1182
+ * button disabled. The declarative shadow roots are still the SSR ones
1183
+ * here — `componentWillLoad` runs before the first client render.
1184
+ */
1185
+ adoptPrehydrationValues(screen) {
1186
+ const root = this.el.shadowRoot;
1187
+ if (!root)
1188
+ return;
1189
+ const roots = [root];
1190
+ root.querySelectorAll("authhero-node").forEach((node) => {
1191
+ if (node.shadowRoot)
1192
+ roots.push(node.shadowRoot);
1193
+ });
1194
+ const adopted = {};
1195
+ for (const component of screen.components ?? []) {
1196
+ if (!PREHYDRATION_TEXT_TYPES.has(component.type))
1197
+ continue;
1198
+ for (const r of roots) {
1199
+ const input = Array.from(r.querySelectorAll("input[name]")).find((el) => el.name === component.id && el.type !== "hidden");
1200
+ // Adopt "" too: a prefilled default the user cleared before
1201
+ // hydration must not be restored by the client render.
1202
+ if (input) {
1203
+ adopted[component.id] = input.value;
1204
+ break;
1205
+ }
1206
+ }
1207
+ }
1208
+ if (Object.keys(adopted).length > 0) {
1209
+ this.formData = { ...this.formData, ...adopted };
1210
+ }
1211
+ }
1132
1212
  /**
1133
1213
  * Fetch screen configuration from the API
1134
1214
  * @param screenIdOverride Optional screen ID to fetch (overrides this.screenId)
@@ -1230,14 +1310,44 @@ const AuthheroWidget = class {
1230
1310
  e.preventDefault();
1231
1311
  if (!this._screen || this.loading)
1232
1312
  return;
1233
- let submitData = { ...this.formData, ...(overrideData || {}) };
1234
- // Merge hidden input values from DOM (may have been set programmatically
1235
- // by inline scripts, e.g. passkey management buttons)
1236
1313
  const form = this.el.shadowRoot?.querySelector("form");
1314
+ if (this._screen.partials &&
1315
+ form?.reportValidity &&
1316
+ !form.reportValidity()) {
1317
+ return;
1318
+ }
1319
+ const submitData = {
1320
+ ...this.formData,
1321
+ ...(overrideData || {}),
1322
+ };
1323
+ // Native fields in prompt partials are direct descendants of this form,
1324
+ // unlike authhero-node inputs in nested shadow roots. Read them through
1325
+ // FormData so standard HTML controls and successful-control semantics are
1326
+ // preserved (disabled controls are skipped, repeated names are kept as
1327
+ // arrays). FormData also covers enabled hidden inputs for ulp-* names.
1328
+ const nativeNames = new Set();
1329
+ if (form && this._screen.partials) {
1330
+ const nativeValues = {};
1331
+ new FormData(form).forEach((value, key) => {
1332
+ if (typeof value === "string" && key.startsWith("ulp-")) {
1333
+ (nativeValues[key] ??= []).push(value);
1334
+ }
1335
+ });
1336
+ for (const [key, values] of Object.entries(nativeValues)) {
1337
+ nativeNames.add(key);
1338
+ submitData[key] = values.length > 1 ? values : values[0];
1339
+ }
1340
+ }
1341
+ // Legacy behaviour for inline scripts that fill hidden inputs: merge
1342
+ // enabled ones, but never overwrite what FormData already collected.
1237
1343
  if (form) {
1238
- const hiddenInputs = form.querySelectorAll('input[type="hidden"]');
1239
- hiddenInputs.forEach((input) => {
1240
- if (input.name && input.value) {
1344
+ form
1345
+ .querySelectorAll('input[type="hidden"]')
1346
+ .forEach((input) => {
1347
+ if (input.name &&
1348
+ input.value &&
1349
+ !input.disabled &&
1350
+ !nativeNames.has(input.name)) {
1241
1351
  submitData[input.name] = input.value;
1242
1352
  }
1243
1353
  });
@@ -1391,6 +1501,11 @@ const AuthheroWidget = class {
1391
1501
  if (!form)
1392
1502
  return;
1393
1503
  form.submit = () => {
1504
+ if (this._screen?.partials &&
1505
+ form.reportValidity &&
1506
+ !form.reportValidity()) {
1507
+ return;
1508
+ }
1394
1509
  const formData = new FormData(form);
1395
1510
  const data = {};
1396
1511
  formData.forEach((value, key) => {
@@ -1995,6 +2110,27 @@ const AuthheroWidget = class {
1995
2110
  const config = component.config;
1996
2111
  return config?.text ?? "";
1997
2112
  }
2113
+ renderPromptPartial(screen, insertionPoint) {
2114
+ const html = screen.partials?.[insertionPoint];
2115
+ if (!html)
2116
+ return null;
2117
+ // Prompt partials intentionally contain tenant-authored form controls, so
2118
+ // the widget must keep their input attributes intact.
2119
+ return (h("div", { class: `prompt-partial prompt-partial-${insertionPoint}`, part: `prompt-partial prompt-partial-${insertionPoint}`, "data-insertion-point": insertionPoint, innerHTML: html }));
2120
+ }
2121
+ renderScreenLinks(screen) {
2122
+ if (!screen.links?.length)
2123
+ return null;
2124
+ return (h("div", { class: "links", part: "links" }, screen.links.map((link) => (h("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (h("span", null, link.text, " ", h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
2125
+ id: link.id,
2126
+ href: link.href,
2127
+ text: link.linkText || link.text,
2128
+ }) }, link.linkText))) : (h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
2129
+ id: link.id,
2130
+ href: link.href,
2131
+ text: link.text,
2132
+ }) }, link.text)))))));
2133
+ }
1998
2134
  render() {
1999
2135
  const screen = this._screen;
2000
2136
  if (this.loading && !screen) {
@@ -2021,13 +2157,20 @@ const AuthheroWidget = class {
2021
2157
  ? components.find((c) => this.isDividerComponent(c))
2022
2158
  : undefined;
2023
2159
  const fieldComponents = components.filter((c) => !this.isSocialComponent(c) && c !== dividerComponent);
2160
+ const hasPromptPartials = Object.values(screen.partials ?? {}).some((html) => !!html);
2161
+ const footerComponents = hasPromptPartials
2162
+ ? fieldComponents.filter((component) => component.id === "submit" && component.type === "NEXT_BUTTON")
2163
+ : [];
2164
+ const contentFieldComponents = hasPromptPartials
2165
+ ? fieldComponents.filter((component) => !footerComponents.includes(component))
2166
+ : fieldComponents;
2024
2167
  const hasDivider = !!dividerComponent;
2025
2168
  const dividerText = dividerComponent?.config?.text || "Or";
2026
2169
  // A "choice list" screen is one whose entire field section is pick-one
2027
2170
  // buttons (e.g. tenant selection). When the list is long, render it as a
2028
2171
  // searchable, scrollable list instead of a tall column of buttons.
2029
- const choiceButtons = fieldComponents.filter((c) => c.type === "NEXT_BUTTON");
2030
- const isSearchableChoiceList = choiceButtons.length === fieldComponents.length &&
2172
+ const choiceButtons = contentFieldComponents.filter((c) => c.type === "NEXT_BUTTON");
2173
+ const isSearchableChoiceList = choiceButtons.length === contentFieldComponents.length &&
2031
2174
  choiceButtons.length > CHOICE_LIST_SEARCH_THRESHOLD;
2032
2175
  // Hold the primary action button disabled until every required field has
2033
2176
  // a value.
@@ -2068,7 +2211,7 @@ const AuthheroWidget = class {
2068
2211
  };
2069
2212
  // Get logo URL from theme.widget (takes precedence) or branding
2070
2213
  const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
2071
- return (h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, action: screen.action, method: screen.method || "POST", part: "form" }, hiddenComponents.map((c) => (h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), h("div", { class: "form-content" }, socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (h("authhero-node", { key: component.id, component: component, locale: this.locale, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
2214
+ return (h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, action: screen.action, method: screen.method || "POST", part: "form" }, hiddenComponents.map((c) => (h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), h("div", { class: "form-content" }, this.renderPromptPartial(screen, "form-content-start"), socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (h("authhero-node", { key: component.id, component: component, locale: this.locale, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
2072
2215
  fieldComponents.length > 0 &&
2073
2216
  hasDivider && (h("div", { class: "divider", part: "divider" }, h("span", { class: "divider-text" }, dividerText))), h("div", { class: "fields-section", part: "fields-section" }, isSearchableChoiceList && (h("input", { type: "text", class: "choice-list-search", part: "choice-list-search", placeholder: "Search", "aria-label": "Search", autocomplete: "off", value: this.listFilter, onInput: (e) => (this.listFilter = e.target.value), onKeyDown: (e) => {
2074
2217
  // The search box lives inside the form; Enter would
@@ -2081,19 +2224,14 @@ const AuthheroWidget = class {
2081
2224
  ? "fields-list fields-list-scroll"
2082
2225
  : "fields-list" }, (isSearchableChoiceList
2083
2226
  ? visibleChoiceButtons
2084
- : fieldComponents).map((component) => (h("authhero-node", { key: component.id, component: component, locale: this.locale, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading ||
2227
+ : contentFieldComponents).map((component) => (h("authhero-node", { key: component.id, component: component, locale: this.locale, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading ||
2085
2228
  (component.type === "NEXT_BUTTON" &&
2086
2229
  component.config?.skip_validation !== true &&
2087
2230
  requiredFieldsMissing) }))), isSearchableChoiceList &&
2088
- visibleChoiceButtons.length === 0 && (h("div", { class: "choice-list-empty", part: "choice-list-empty" }, "No matches")))))), screen.links && screen.links.length > 0 && (h("div", { class: "links", part: "links" }, screen.links.map((link) => (h("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (h("span", null, link.text, " ", h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
2089
- id: link.id,
2090
- href: link.href,
2091
- text: link.linkText || link.text,
2092
- }) }, link.linkText))) : (h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
2093
- id: link.id,
2094
- href: link.href,
2095
- text: link.text,
2096
- }) }, link.text))))))), screen.footer && (h("div", { class: "widget-footer", part: "footer", innerHTML: sanitizeHtml(screen.footer) })))));
2231
+ visibleChoiceButtons.length === 0 && (h("div", { class: "choice-list-empty", part: "choice-list-empty" }, "No matches")))), this.renderPromptPartial(screen, "form-content-end"), hasPromptPartials && (h("div", { class: "form-footer", part: "form-footer" }, this.renderPromptPartial(screen, "form-footer-start"), footerComponents.map((component) => (h("authhero-node", { key: component.id, component: component, locale: this.locale, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading ||
2232
+ (component.type === "NEXT_BUTTON" &&
2233
+ component.config?.skip_validation !== true &&
2234
+ requiredFieldsMissing) }))), this.renderPromptPartial(screen, "form-footer-end")))), hasPromptPartials && (h("div", { class: "secondary-actions", part: "secondary-actions" }, this.renderPromptPartial(screen, "secondary-actions-start"), this.renderScreenLinks(screen), this.renderPromptPartial(screen, "secondary-actions-end")))), !hasPromptPartials && this.renderScreenLinks(screen), screen.footer && (h("div", { class: "widget-footer", part: "footer", innerHTML: sanitizeHtml(screen.footer) })))));
2097
2235
  }
2098
2236
  static get watchers() { return {
2099
2237
  "screenId": [{