@atlasauth/embed 0.2.0 → 0.2.2
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/CHANGELOG.md +48 -0
- package/dist/appearance.d.ts +22 -0
- package/dist/appearance.js +14 -0
- package/dist/appearance.js.map +1 -1
- package/dist/embed.js +17 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +15 -6
- package/dist/styles.js.map +1 -1
- package/dist/widget/captcha.d.ts +12 -18
- package/dist/widget/captcha.js +14 -188
- package/dist/widget/captcha.js.map +1 -1
- package/dist/widget/forms.d.ts +2 -0
- package/dist/widget/forms.js +11 -2
- package/dist/widget/forms.js.map +1 -1
- package/dist/widget.d.ts +4 -0
- package/dist/widget.js +10 -0
- package/dist/widget.js.map +1 -1
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,6 @@ export { defineElements, AtlasSignInElement, AtlasSignUpElement } from './elemen
|
|
|
15
15
|
export { mountSignIn, mountSignUp, type MountTarget } from './mount';
|
|
16
16
|
export { setScriptDefaults, readScriptTag, type ScriptDefaults } from './config';
|
|
17
17
|
export { validateRedirect } from './redirect';
|
|
18
|
-
export { resolveTokens, cssVariables, orderedProviders, providerLabel, classFor, resolveSocialButtons, type DesignTokens, type ElementKey, type WidgetAppearance, type PublicAppearance, type PublicStrategies, type AppearanceResponse, type SocialButtonsConfig, type SocialButtonVariant, type SocialButtonSize, type SocialButtonLayout, type ResolvedSocialButtons, } from './appearance';
|
|
18
|
+
export { resolveTokens, cssVariables, orderedProviders, providerLabel, classFor, resolveSocialButtons, type DesignTokens, type ElementKey, type WidgetAppearance, type PublicAppearance, type PublicStrategies, type AppearanceResponse, type EmailDeliveryAvailability, type SocialButtonsConfig, type SocialButtonVariant, type SocialButtonSize, type SocialButtonLayout, type ResolvedSocialButtons, } from './appearance';
|
|
19
19
|
export { providerIcon } from './icons';
|
|
20
20
|
export { widgetDesign, type WidgetDesign } from './design';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;GAWG;AACH,mCAA2D;AAAlD,qGAAA,WAAW,OAAA;AACpB,qCAAmF;AAA1E,yGAAA,cAAc,OAAA;AAAE,6GAAA,kBAAkB,OAAA;AAAE,6GAAA,kBAAkB,OAAA;AAC/D,iCAAqE;AAA5D,oGAAA,WAAW,OAAA;AAAE,oGAAA,WAAW,OAAA;AACjC,mCAAiF;AAAxE,2GAAA,iBAAiB,OAAA;AAAE,uGAAA,aAAa,OAAA;AACzC,uCAA8C;AAArC,4GAAA,gBAAgB,OAAA;AACzB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;GAWG;AACH,mCAA2D;AAAlD,qGAAA,WAAW,OAAA;AACpB,qCAAmF;AAA1E,yGAAA,cAAc,OAAA;AAAE,6GAAA,kBAAkB,OAAA;AAAE,6GAAA,kBAAkB,OAAA;AAC/D,iCAAqE;AAA5D,oGAAA,WAAW,OAAA;AAAE,oGAAA,WAAW,OAAA;AACjC,mCAAiF;AAAxE,2GAAA,iBAAiB,OAAA;AAAE,uGAAA,aAAa,OAAA;AACzC,uCAA8C;AAArC,4GAAA,gBAAgB,OAAA;AACzB,2CAmBsB;AAlBpB,2GAAA,aAAa,OAAA;AACb,0GAAA,YAAY,OAAA;AACZ,8GAAA,gBAAgB,OAAA;AAChB,2GAAA,aAAa,OAAA;AACb,sGAAA,QAAQ,OAAA;AACR,kHAAA,oBAAoB,OAAA;AActB,iCAAuC;AAA9B,qGAAA,YAAY,OAAA;AACrB,mCAA2D;AAAlD,sGAAA,YAAY,OAAA"}
|
package/dist/styles.d.ts
CHANGED
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
* before `GET /v1/appearance` resolves — and re-themes when it does.
|
|
13
13
|
*/
|
|
14
14
|
export declare const STYLE_MARKER = "atlas-embed-styles";
|
|
15
|
-
export declare const EMBED_CSS = "\n.atlas-embed-root {\n --_bg: var(--atlas-embed-color-background, #ffffff);\n --_card: var(--atlas-embed-color-card, #ffffff);\n --_text: var(--atlas-embed-color-text, #1c1c1f);\n --_primary: var(--atlas-embed-color-primary, #5b5bd6);\n --_border: var(--atlas-embed-color-border, #e4e4e9);\n --_radius: var(--atlas-embed-border-radius, 10px);\n /* Transition tempo \u2014 the widget sets --atlas-embed-motion from motionSpeed and\n to 0s when animations are off; reduced-motion forces 0s below regardless. */\n --_motion: var(--atlas-embed-motion, 0.16s);\n font-family: var(--atlas-embed-font-family, ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n color: var(--_text);\n box-sizing: border-box;\n display: block;\n}\n.atlas-embed-root *, .atlas-embed-root *::before, .atlas-embed-root *::after { box-sizing: border-box; }\n.atlas-embed-card {\n max-width: 400px;\n margin: 0 auto;\n padding: 28px 26px;\n background: var(--_card);\n border: 1px solid var(--_border);\n border-radius: calc(var(--_radius) + 4px);\n box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 8px 28px rgba(0,0,0,0.06);\n}\n.atlas-embed-logo { display: block; max-height: 40px; margin: 0 auto 16px; }\n.atlas-embed-title { font-size: 20px; font-weight: 650; text-align: center; margin: 0 0 4px; }\n.atlas-embed-subtitle { font-size: 14px; color: #6b7280; text-align: center; margin: 0 0 20px; }\n.atlas-embed-social { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }\n.atlas-embed-social-button {\n display: flex; align-items: center; justify-content: center; gap: 8px;\n width: 100%; padding: 10px 12px; font-size: 14px; font-weight: 550; cursor: pointer;\n color: var(--_text); background: var(--_card);\n border: 1px solid var(--_border); border-radius: var(--_radius);\n}\n.atlas-embed-social-button:hover { filter: brightness(0.97); }\n.atlas-embed-social-icon { display: inline-flex; flex: none; line-height: 0; }\n.atlas-embed-social-icon svg { width: 18px; height: 18px; display: block; }\n.atlas-embed-social-label { display: inline-block; }\n\n/* Size \u2014 button scale and icon size together. */\n.atlas-embed-social[data-size=\"sm\"] .atlas-embed-social-button { padding: 8px 10px; font-size: 13px; }\n.atlas-embed-social[data-size=\"sm\"] .atlas-embed-social-icon svg { width: 16px; height: 16px; }\n.atlas-embed-social[data-size=\"lg\"] .atlas-embed-social-button { padding: 13px 16px; font-size: 15px; }\n.atlas-embed-social[data-size=\"lg\"] .atlas-embed-social-icon svg { width: 22px; height: 22px; }\n\n/* Icon-only variant: a square button, name in the tooltip. Always laid out as a\n centred wrap (or a carousel), never a full-width bar. */\n.atlas-embed-social-button[data-variant=\"icon\"] { width: auto; padding: 10px; aspect-ratio: 1; }\n.atlas-embed-social[data-size=\"sm\"] .atlas-embed-social-button[data-variant=\"icon\"] { padding: 8px; }\n.atlas-embed-social[data-size=\"lg\"] .atlas-embed-social-button[data-variant=\"icon\"] { padding: 13px; }\n.atlas-embed-social[data-variant=\"icon\"]:not([data-layout=\"carousel\"]) {\n flex-direction: row; flex-wrap: wrap; justify-content: center;\n}\n\n/* Grid \u2014 bars share a row and wrap. */\n.atlas-embed-social[data-layout=\"grid\"] { flex-direction: row; flex-wrap: wrap; }\n.atlas-embed-social[data-layout=\"grid\"] .atlas-embed-social-button { width: auto; flex: 1 1 auto; }\n.atlas-embed-social[data-layout=\"grid\"] .atlas-embed-social-button[data-variant=\"icon\"] { flex: 0 0 auto; }\n\n/* Carousel \u2014 one swipeable, scroll-snapped row. The raw scrollbar is hidden; the\n arrow buttons (below) are the affordance instead. */\n.atlas-embed-social[data-layout=\"carousel\"] {\n flex-direction: row; flex-wrap: nowrap; overflow-x: auto;\n scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; scroll-behavior: smooth;\n scrollbar-width: none;\n}\n.atlas-embed-social[data-layout=\"carousel\"]::-webkit-scrollbar { display: none; }\n.atlas-embed-social[data-layout=\"carousel\"] .atlas-embed-social-button { width: auto; flex: 0 0 auto; scroll-snap-align: start; }\n.atlas-embed-social[data-layout=\"carousel\"] .atlas-embed-social-button[data-variant=\"block\"] { min-width: 60%; }\n\n/* Carousel shell \u2014 prev/next controls flanking the scroll row. */\n.atlas-embed-carousel { position: relative; display: flex; align-items: center; gap: 4px; margin-bottom: 16px; }\n.atlas-embed-carousel .atlas-embed-social { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }\n.atlas-embed-carousel-nav {\n flex: none; display: inline-flex; align-items: center; justify-content: center;\n width: 30px; height: 30px; padding: 0; cursor: pointer;\n color: var(--_text); background: var(--_card);\n border: 1px solid var(--_border); border-radius: 999px; transition: opacity 0.15s;\n}\n.atlas-embed-carousel-nav svg { width: 16px; height: 16px; display: block; }\n.atlas-embed-carousel-nav:hover:not(:disabled) { filter: brightness(0.97); }\n.atlas-embed-carousel-nav:disabled { opacity: 0.35; cursor: default; }\n/* Nothing overflows \u2192 no controls at all. */\n.atlas-embed-carousel[data-overflow=\"false\"] .atlas-embed-carousel-nav { display: none; }\n/* When the carousel sits inside a search field, the field owns the bottom margin. */\n.atlas-embed-social-field .atlas-embed-carousel { margin-bottom: 0; }\n\n/* Search \u2014 a filter box above a long provider list (socialButtons.search). The\n field wraps the button container so its own margin replaces the button block's. */\n.atlas-embed-social-field { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }\n.atlas-embed-social-field .atlas-embed-social { margin-bottom: 0; }\n.atlas-embed-social-search {\n width: 100%; padding: 9px 12px; font-size: 14px; color: var(--_text);\n background: var(--_bg); border: 1px solid var(--_border); border-radius: var(--_radius);\n}\n.atlas-embed-social-search:focus { outline: 2px solid var(--_primary); outline-offset: -1px; }\n.atlas-embed-social-button[hidden] { display: none; }\n.atlas-embed-social-empty { font-size: 13px; color: #9ca3af; text-align: center; padding: 6px 0; }\n.atlas-embed-social-empty[hidden] { display: none; }\n.atlas-embed-divider {\n display: flex; align-items: center; text-align: center; gap: 10px;\n color: #9ca3af; font-size: 12px; margin: 14px 0;\n}\n.atlas-embed-divider::before, .atlas-embed-divider::after {\n content: \"\"; flex: 1; height: 1px; background: var(--_border);\n}\n.atlas-embed-field { display: block; margin: 0 0 12px; }\n.atlas-embed-label { display: block; font-size: 13px; margin: 0 0 5px; color: var(--_text); }\n.atlas-embed-input {\n width: 100%; padding: 10px 12px; font-size: 14px; color: var(--_text);\n background: var(--_bg); border: 1px solid var(--_border); border-radius: var(--_radius);\n}\n.atlas-embed-input:focus { outline: 2px solid var(--_primary); outline-offset: -1px; }\n/* Password field with a show/hide toggle. */\n.atlas-embed-password { position: relative; display: flex; align-items: center; }\n.atlas-embed-password .atlas-embed-input { width: 100%; padding-right: 54px; }\n.atlas-embed-password-toggle {\n position: absolute; right: 6px; padding: 4px 7px; font: inherit; font-size: 12px; font-weight: 600;\n cursor: pointer; color: var(--_primary); background: none; border: 0; border-radius: 6px;\n}\n.atlas-embed-password-toggle:hover { text-decoration: underline; }\nselect.atlas-embed-input { appearance: auto; }\n.atlas-embed-field-checkbox { display: flex; flex-direction: row-reverse; align-items: center; justify-content: flex-end; gap: 8px; }\n.atlas-embed-field-checkbox .atlas-embed-label { margin: 0; }\n.atlas-embed-checkbox { width: 16px; height: 16px; margin: 0; flex: none; }\ntextarea.atlas-embed-input { min-height: 72px; resize: vertical; font-family: inherit; }\nfieldset.atlas-embed-field { border: 0; padding: 0; margin: 0 0 12px; }\n.atlas-embed-choices { display: flex; flex-direction: column; gap: 6px; }\n.atlas-embed-choice { display: flex; flex-direction: row; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; }\n.atlas-embed-choice input { width: 16px; height: 16px; margin: 0; flex: none; accent-color: var(--_primary); }\n.atlas-embed-button {\n width: 100%; margin-top: 6px; padding: 11px 12px; font-size: 14px; font-weight: 650;\n cursor: pointer; color: #ffffff; background: var(--_primary);\n border: 0; border-radius: var(--_radius);\n}\n.atlas-embed-button:disabled { opacity: 0.6; cursor: default; }\n.atlas-embed-handle-back {\n width: 100%; margin-top: 8px; padding: 6px; font-size: 13px; font-weight: 550;\n cursor: pointer; color: #6b7280; background: none; border: 0;\n}\n.atlas-embed-handle-back:hover { color: #374151; }\n.atlas-embed-error { display: block; font-size: 13px; color: #d64545; margin: 6px 0 0; }\n.atlas-embed-form-error { font-size: 13px; color: #d64545; margin: 0 0 12px; min-height: 0; }\n/* A success/confirmation banner (e.g. after a force-login password reset). */\n.atlas-embed-flash {\n font-size: 13px; color: #0f7a4d; background: rgba(16,163,101,0.08);\n border: 1px solid rgba(16,163,101,0.25); border-radius: var(--_radius);\n padding: 9px 11px; margin: 0 0 14px; text-align: center;\n}\n.atlas-embed-footer { font-size: 13px; color: #6b7280; text-align: center; margin: 16px 0 0; }\n/* \u00A75.1 legal consent row on sign-up. */\n.atlas-embed-consent { display: flex; align-items: flex-start; gap: 8px; margin: 2px 0 14px; font-size: 13px; color: #6b7280; }\n.atlas-embed-consent-check { margin: 2px 0 0; width: 15px; height: 15px; flex: none; accent-color: var(--_primary); cursor: pointer; }\n.atlas-embed-consent-link { color: var(--_primary); text-decoration: underline; }\n/* \u00A77.5 Remember-me row. */\n.atlas-embed-remember { display: flex; align-items: center; gap: 8px; margin: -2px 0 14px; font-size: 13px; color: var(--_text); cursor: pointer; }\n.atlas-embed-remember-check { width: 15px; height: 15px; flex: none; accent-color: var(--_primary); cursor: pointer; }\n.atlas-embed-note { font-size: 14px; color: #6b7280; text-align: center; margin: 8px 0; }\n\n/* Verification step \u2014 a continuation of the email panel, not a new screen: the\n context line (\"Enter the code we sent to \u2026\") + a way back, then a segmented\n one-box-per-digit code input that spawns in. */\n.atlas-embed-step-context { text-align: center; margin: 0 0 20px; }\n.atlas-embed-step-context .atlas-embed-subtitle { margin: 0 0 6px; }\n.atlas-embed-step-back {\n display: inline-block; padding: 2px 4px; font: inherit; font-size: 13px; font-weight: 550;\n cursor: pointer; color: var(--_primary); background: none; border: 0;\n}\n.atlas-embed-step-back:hover { text-decoration: underline; }\n/* A standalone action under the submit button (Forgot password?, Back to sign in,\n the 2FA toggle) reads as a centred link, not a left-aligned fragment. */\nform > .atlas-embed-step-back { display: block; width: 100%; text-align: center; margin-top: 10px; }\n.atlas-embed-code { display: flex; gap: 8px; justify-content: space-between; }\n.atlas-embed-code-box {\n flex: 1 1 0; min-width: 0; aspect-ratio: 1 / 1.15; padding: 0; text-align: center;\n font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--_text);\n background: var(--_bg); border: 1px solid var(--_border); border-radius: var(--_radius);\n}\n.atlas-embed-code-box:focus { outline: 2px solid var(--_primary); outline-offset: -1px; }\n/* A real step change eases its content in; error/busy re-renders (same step) do\n not, and a reduced-motion viewer gets no animation at all. */\n@media (prefers-reduced-motion: no-preference) {\n .atlas-embed-card[data-enter] > form,\n .atlas-embed-card[data-enter] > .atlas-embed-step-context {\n animation: atlas-embed-spawn 0.26s cubic-bezier(0.22, 0.61, 0.36, 1) both;\n }\n}\n@keyframes atlas-embed-spawn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Interaction / polish layer \u2014 how controls FEEL. Driven by data-attributes on\n the root (data-button-style / -button-effect / -shadow / -density /\n -input-style) that the widget sets from the resolved interaction config, plus\n the --_motion tempo var. Every rule is additive: with no attributes set the\n widget looks exactly as it did before this layer existed.\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Smooth every interactive control at the configured tempo. */\n.atlas-embed-button, .atlas-embed-social-button, .atlas-embed-carousel-nav,\n.atlas-embed-handle-back, .atlas-embed-step-back, .atlas-embed-password-toggle,\n.atlas-embed-input, .atlas-embed-code-box {\n transition:\n transform var(--_motion) ease, box-shadow var(--_motion) ease,\n background-color var(--_motion) ease, border-color var(--_motion) ease,\n color var(--_motion) ease, filter var(--_motion) ease, outline-color var(--_motion) ease;\n}\n\n/* Primary CTA \u2014 a subtle hover darken by default (was static). */\n.atlas-embed-button:hover:not(:disabled) { filter: brightness(1.06); }\n\n/* Button fill style (primary CTA only \u2014 social buttons keep provider styling). */\n.atlas-embed-root[data-button-style=\"outline\"] .atlas-embed-button {\n background: transparent; color: var(--_primary); border: 1px solid var(--_primary);\n}\n.atlas-embed-root[data-button-style=\"outline\"] .atlas-embed-button:hover:not(:disabled) {\n filter: none; background: color-mix(in srgb, var(--_primary) 10%, transparent);\n}\n.atlas-embed-root[data-button-style=\"soft\"] .atlas-embed-button {\n background: color-mix(in srgb, var(--_primary) 14%, transparent); color: var(--_primary);\n}\n.atlas-embed-root[data-button-style=\"soft\"] .atlas-embed-button:hover:not(:disabled) {\n filter: none; background: color-mix(in srgb, var(--_primary) 22%, transparent);\n}\n.atlas-embed-root[data-button-style=\"ghost\"] .atlas-embed-button {\n background: transparent; color: var(--_primary);\n}\n.atlas-embed-root[data-button-style=\"ghost\"] .atlas-embed-button:hover:not(:disabled) {\n filter: none; background: color-mix(in srgb, var(--_primary) 12%, transparent);\n}\n\n/* Press/hover effect \u2014 applies to every button. */\n.atlas-embed-root[data-button-effect=\"scale\"] .atlas-embed-button:active:not(:disabled),\n.atlas-embed-root[data-button-effect=\"scale\"] .atlas-embed-social-button:active,\n.atlas-embed-root[data-button-effect=\"scale\"] .atlas-embed-carousel-nav:active:not(:disabled) {\n transform: scale(0.97);\n}\n.atlas-embed-root[data-button-effect=\"lift\"] .atlas-embed-button:hover:not(:disabled),\n.atlas-embed-root[data-button-effect=\"lift\"] .atlas-embed-social-button:hover {\n transform: translateY(-1px); box-shadow: 0 4px 14px rgba(0,0,0,0.14); filter: none;\n}\n.atlas-embed-root[data-button-effect=\"lift\"] .atlas-embed-button:active:not(:disabled),\n.atlas-embed-root[data-button-effect=\"lift\"] .atlas-embed-social-button:active { transform: translateY(0); }\n.atlas-embed-root[data-button-effect=\"glow\"] .atlas-embed-button:hover:not(:disabled),\n.atlas-embed-root[data-button-effect=\"glow\"] .atlas-embed-social-button:hover {\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--_primary) 35%, transparent); filter: none;\n}\n/* Ripple \u2014 the widget appends a span on pointer-down; buttons must clip it. */\n.atlas-embed-root[data-button-effect=\"ripple\"] .atlas-embed-button,\n.atlas-embed-root[data-button-effect=\"ripple\"] .atlas-embed-social-button {\n position: relative; overflow: hidden;\n}\n.atlas-embed-ripple {\n position: absolute; border-radius: 50%; transform: scale(0);\n background: currentColor; opacity: 0.28; pointer-events: none;\n animation: atlas-embed-ripple 0.55s ease-out;\n}\n@keyframes atlas-embed-ripple { to { transform: scale(2.6); opacity: 0; } }\n\n/* Card + solid-button elevation. */\n.atlas-embed-root[data-shadow=\"none\"] .atlas-embed-card { box-shadow: none; }\n.atlas-embed-root[data-shadow=\"sm\"] .atlas-embed-card { box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\n.atlas-embed-root[data-shadow=\"lg\"] .atlas-embed-card {\n box-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 16px 48px rgba(0,0,0,0.12);\n}\n\n/* Density \u2014 compact tightens padding and gaps. */\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-card { padding: 20px 18px; }\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-social { gap: 6px; margin-bottom: 12px; }\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-field { margin-bottom: 9px; }\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-button { padding: 9px 12px; }\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-social-button { padding: 8px 10px; }\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-subtitle { margin-bottom: 14px; }\n\n/* Input style \u2014 filled and underline treatments (default is the outlined field). */\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-input,\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-code-box,\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-social-search {\n background: color-mix(in srgb, var(--_border) 24%, transparent); border-color: transparent;\n}\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-input:focus,\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-code-box:focus,\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-social-search:focus {\n background: var(--_bg); border-color: var(--_primary);\n}\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-input,\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-code-box,\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-social-search {\n background: transparent; border: 0; border-bottom: 1.5px solid var(--_border);\n border-radius: 0; padding-left: 2px; padding-right: 2px;\n}\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-input:focus,\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-code-box:focus,\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-social-search:focus {\n outline: none; border-bottom-color: var(--_primary);\n}\n\n/* \u2500\u2500 Loading states \u2014 spinner / skeleton / dots / bar (interaction.loading).\n Shown while appearance resolves, while a redirect ticket is exchanged, and the\n button spinner while a form submits \u2014 so the widget never flashes blank. \u2500\u2500 */\n.atlas-embed-button { display: flex; align-items: center; justify-content: center; gap: 8px; }\n.atlas-embed-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; min-height: 168px; padding: 12px 0; }\n.atlas-embed-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }\n\n/* Spinner (default) + the in-button spinner. */\n.atlas-embed-spinner, .atlas-embed-btn-spinner {\n border-radius: 50%; border-style: solid; flex: none;\n animation: atlas-embed-spin 0.7s linear infinite;\n}\n.atlas-embed-spinner { width: 30px; height: 30px; border-width: 3px;\n border-color: color-mix(in srgb, var(--_primary) 22%, transparent); border-top-color: var(--_primary); }\n.atlas-embed-btn-spinner { width: 17px; height: 17px; border-width: 2px;\n border-color: color-mix(in srgb, currentColor 35%, transparent); border-top-color: currentColor; }\n@keyframes atlas-embed-spin { to { transform: rotate(360deg); } }\n\n/* Dots. */\n.atlas-embed-loading[data-loading=\"dots\"] { flex-direction: row; }\n.atlas-embed-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--_primary);\n animation: atlas-embed-bounce 1.2s ease-in-out infinite; }\n.atlas-embed-dot:nth-child(2) { animation-delay: 0.15s; }\n.atlas-embed-dot:nth-child(3) { animation-delay: 0.30s; }\n@keyframes atlas-embed-bounce { 0%,80%,100% { transform: scale(0.5); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }\n\n/* Indeterminate bar. */\n.atlas-embed-loading[data-loading=\"bar\"] { min-height: 132px; }\n.atlas-embed-loadbar { position: relative; width: 64%; height: 4px; border-radius: 999px; overflow: hidden;\n background: color-mix(in srgb, var(--_border) 60%, transparent); }\n.atlas-embed-loadbar::before { content: \"\"; position: absolute; top: 0; bottom: 0; width: 40%; border-radius: 999px;\n background: var(--_primary); animation: atlas-embed-slide 1.1s ease-in-out infinite; }\n@keyframes atlas-embed-slide { 0% { left: -45%; } 100% { left: 105%; } }\n\n/* Skeleton \u2014 placeholders shaped like the form. */\n.atlas-embed-loading[data-loading=\"skeleton\"] { align-items: stretch; min-height: 0; gap: 12px; }\n.atlas-embed-skel { border-radius: var(--_radius); position: relative; overflow: hidden;\n background: color-mix(in srgb, var(--_border) 55%, transparent); }\n.atlas-embed-skel::after { content: \"\"; position: absolute; inset: 0; transform: translateX(-100%);\n background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--_text) 8%, transparent), transparent);\n animation: atlas-embed-shimmer 1.3s infinite; }\n.atlas-embed-skel-title { height: 22px; width: 55%; margin: 2px auto 2px; }\n.atlas-embed-skel-sub { height: 13px; width: 75%; margin: 0 auto 8px; }\n.atlas-embed-skel-input { height: 42px; }\n.atlas-embed-skel-button { height: 44px; margin-top: 6px; }\n@keyframes atlas-embed-shimmer { to { transform: translateX(100%); } }\n\n/* Reduced motion always wins \u2014 no transitions, presses, ripples, or spawn; and\n loaders swap their spin/bounce/slide/shimmer for a minimal opacity pulse. */\n@media (prefers-reduced-motion: reduce) {\n .atlas-embed-root { --_motion: 0s; }\n .atlas-embed-root [class^=\"atlas-embed-\"] { transition: none !important; }\n .atlas-embed-root .atlas-embed-button:active,\n .atlas-embed-root .atlas-embed-social-button:active { transform: none !important; }\n .atlas-embed-ripple { display: none; }\n .atlas-embed-spinner, .atlas-embed-btn-spinner, .atlas-embed-dot,\n .atlas-embed-loadbar::before, .atlas-embed-skel::after {\n animation: atlas-embed-pulse 1.4s ease-in-out infinite !important;\n }\n .atlas-embed-loadbar::before { position: static; width: 100%; }\n}\n@keyframes atlas-embed-pulse { 0%,100% { opacity: 0.4; } 50% { opacity: 1; } }\n";
|
|
15
|
+
export declare const EMBED_CSS = "\n.atlas-embed-root {\n --_bg: var(--atlas-embed-color-background, #ffffff);\n --_card: var(--atlas-embed-color-card, #ffffff);\n --_text: var(--atlas-embed-color-text, #1c1c1f);\n --_primary: var(--atlas-embed-color-primary, #5b5bd6);\n --_on-primary: var(--atlas-embed-color-text-on-primary, #ffffff);\n /* Keyboard focus ring \u2014 a solid outline (3:1 on the default background). */\n --_focus: var(--atlas-embed-color-focus-ring, var(--_primary));\n --_border: var(--atlas-embed-color-border, #e4e4e9);\n --_radius: var(--atlas-embed-border-radius, 10px);\n /* Transition tempo \u2014 the widget sets --atlas-embed-motion from motionSpeed and\n to 0s when animations are off; reduced-motion forces 0s below regardless. */\n --_motion: var(--atlas-embed-motion, 0.16s);\n font-family: var(--atlas-embed-font-family, ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n color: var(--_text);\n box-sizing: border-box;\n display: block;\n}\n.atlas-embed-root *, .atlas-embed-root *::before, .atlas-embed-root *::after { box-sizing: border-box; }\n.atlas-embed-card {\n max-width: 400px;\n margin: 0 auto;\n padding: 28px 26px;\n background: var(--_card);\n border: 1px solid var(--_border);\n border-radius: calc(var(--_radius) + 4px);\n box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 8px 28px rgba(0,0,0,0.06);\n}\n.atlas-embed-logo { display: block; max-height: 40px; margin: 0 auto 16px; }\n.atlas-embed-title { font-size: 20px; font-weight: 650; text-align: center; margin: 0 0 4px; }\n.atlas-embed-subtitle { font-size: 14px; color: #6b7280; text-align: center; margin: 0 0 20px; }\n.atlas-embed-social { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }\n.atlas-embed-social-button {\n display: flex; align-items: center; justify-content: center; gap: 8px;\n width: 100%; padding: 10px 12px; font-size: 14px; font-weight: 550; cursor: pointer;\n color: var(--_text); background: var(--_card);\n border: 1px solid var(--_border); border-radius: var(--_radius);\n}\n.atlas-embed-social-button:hover { filter: brightness(0.97); }\n.atlas-embed-social-icon { display: inline-flex; flex: none; line-height: 0; }\n.atlas-embed-social-icon svg { width: 18px; height: 18px; display: block; }\n.atlas-embed-social-label { display: inline-block; }\n\n/* Size \u2014 button scale and icon size together. */\n.atlas-embed-social[data-size=\"sm\"] .atlas-embed-social-button { padding: 8px 10px; font-size: 13px; }\n.atlas-embed-social[data-size=\"sm\"] .atlas-embed-social-icon svg { width: 16px; height: 16px; }\n.atlas-embed-social[data-size=\"lg\"] .atlas-embed-social-button { padding: 13px 16px; font-size: 15px; }\n.atlas-embed-social[data-size=\"lg\"] .atlas-embed-social-icon svg { width: 22px; height: 22px; }\n\n/* Icon-only variant: a square button, name in the tooltip. Always laid out as a\n centred wrap (or a carousel), never a full-width bar. */\n.atlas-embed-social-button[data-variant=\"icon\"] { width: auto; padding: 10px; aspect-ratio: 1; }\n.atlas-embed-social[data-size=\"sm\"] .atlas-embed-social-button[data-variant=\"icon\"] { padding: 8px; }\n.atlas-embed-social[data-size=\"lg\"] .atlas-embed-social-button[data-variant=\"icon\"] { padding: 13px; }\n.atlas-embed-social[data-variant=\"icon\"]:not([data-layout=\"carousel\"]) {\n flex-direction: row; flex-wrap: wrap; justify-content: center;\n}\n\n/* Grid \u2014 bars share a row and wrap. */\n.atlas-embed-social[data-layout=\"grid\"] { flex-direction: row; flex-wrap: wrap; }\n.atlas-embed-social[data-layout=\"grid\"] .atlas-embed-social-button { width: auto; flex: 1 1 auto; }\n.atlas-embed-social[data-layout=\"grid\"] .atlas-embed-social-button[data-variant=\"icon\"] { flex: 0 0 auto; }\n\n/* Carousel \u2014 one swipeable, scroll-snapped row. The raw scrollbar is hidden; the\n arrow buttons (below) are the affordance instead. */\n.atlas-embed-social[data-layout=\"carousel\"] {\n flex-direction: row; flex-wrap: nowrap; overflow-x: auto;\n scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; scroll-behavior: smooth;\n scrollbar-width: none;\n}\n.atlas-embed-social[data-layout=\"carousel\"]::-webkit-scrollbar { display: none; }\n.atlas-embed-social[data-layout=\"carousel\"] .atlas-embed-social-button { width: auto; flex: 0 0 auto; scroll-snap-align: start; }\n.atlas-embed-social[data-layout=\"carousel\"] .atlas-embed-social-button[data-variant=\"block\"] { min-width: 60%; }\n\n/* Carousel shell \u2014 prev/next controls flanking the scroll row. */\n.atlas-embed-carousel { position: relative; display: flex; align-items: center; gap: 4px; margin-bottom: 16px; }\n.atlas-embed-carousel .atlas-embed-social { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }\n.atlas-embed-carousel-nav {\n flex: none; display: inline-flex; align-items: center; justify-content: center;\n width: 30px; height: 30px; padding: 0; cursor: pointer;\n color: var(--_text); background: var(--_card);\n border: 1px solid var(--_border); border-radius: 999px; transition: opacity 0.15s;\n}\n.atlas-embed-carousel-nav svg { width: 16px; height: 16px; display: block; }\n.atlas-embed-carousel-nav:hover:not(:disabled) { filter: brightness(0.97); }\n.atlas-embed-carousel-nav:disabled { opacity: 0.35; cursor: default; }\n/* Nothing overflows \u2192 no controls at all. */\n.atlas-embed-carousel[data-overflow=\"false\"] .atlas-embed-carousel-nav { display: none; }\n/* When the carousel sits inside a search field, the field owns the bottom margin. */\n.atlas-embed-social-field .atlas-embed-carousel { margin-bottom: 0; }\n\n/* Search \u2014 a filter box above a long provider list (socialButtons.search). The\n field wraps the button container so its own margin replaces the button block's. */\n.atlas-embed-social-field { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }\n.atlas-embed-social-field .atlas-embed-social { margin-bottom: 0; }\n.atlas-embed-social-search {\n width: 100%; padding: 9px 12px; font-size: 16px; color: var(--_text);\n background: var(--_bg); border: 1px solid var(--_border); border-radius: var(--_radius);\n}\n.atlas-embed-social-search:focus { outline: 2px solid var(--_focus); outline-offset: -1px; }\n.atlas-embed-social-button[hidden] { display: none; }\n.atlas-embed-social-empty { font-size: 13px; color: #9ca3af; text-align: center; padding: 6px 0; }\n.atlas-embed-social-empty[hidden] { display: none; }\n.atlas-embed-divider {\n display: flex; align-items: center; text-align: center; gap: 10px;\n color: #9ca3af; font-size: 12px; margin: 14px 0;\n}\n.atlas-embed-divider::before, .atlas-embed-divider::after {\n content: \"\"; flex: 1; height: 1px; background: var(--_border);\n}\n.atlas-embed-field { display: block; margin: 0 0 12px; }\n.atlas-embed-label { display: block; font-size: 13px; margin: 0 0 5px; color: var(--_text); }\n.atlas-embed-input {\n /* 16px: iOS Safari zooms the page when a smaller field takes focus. */\n width: 100%; padding: 10px 12px; font-size: 16px; color: var(--_text);\n background: var(--_bg); border: 1px solid var(--_border); border-radius: var(--_radius);\n}\n.atlas-embed-input:focus { outline: 2px solid var(--_focus); outline-offset: -1px; }\n/* Password field with a show/hide toggle. */\n.atlas-embed-password { position: relative; display: flex; align-items: center; }\n.atlas-embed-password .atlas-embed-input { width: 100%; padding-right: 54px; }\n.atlas-embed-password-toggle {\n position: absolute; right: 6px; padding: 4px 7px; font: inherit; font-size: 12px; font-weight: 600;\n cursor: pointer; color: var(--_primary); background: none; border: 0; border-radius: 6px;\n}\n.atlas-embed-password-toggle:hover { text-decoration: underline; }\nselect.atlas-embed-input { appearance: auto; }\n.atlas-embed-field-checkbox { display: flex; flex-direction: row-reverse; align-items: center; justify-content: flex-end; gap: 8px; }\n.atlas-embed-field-checkbox .atlas-embed-label { margin: 0; }\n.atlas-embed-checkbox { width: 16px; height: 16px; margin: 0; flex: none; }\ntextarea.atlas-embed-input { min-height: 72px; resize: vertical; font-family: inherit; }\nfieldset.atlas-embed-field { border: 0; padding: 0; margin: 0 0 12px; }\n.atlas-embed-choices { display: flex; flex-direction: column; gap: 6px; }\n.atlas-embed-choice { display: flex; flex-direction: row; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; }\n.atlas-embed-choice input { width: 16px; height: 16px; margin: 0; flex: none; accent-color: var(--_primary); }\n.atlas-embed-button {\n width: 100%; margin-top: 6px; padding: 11px 12px; font-size: 14px; font-weight: 650;\n cursor: pointer; color: var(--_on-primary); background: var(--_primary);\n border: 0; border-radius: var(--_radius);\n}\n.atlas-embed-button:disabled { opacity: 0.6; cursor: default; }\n.atlas-embed-button:focus-visible, .atlas-embed-social-button:focus-visible,\n.atlas-embed-handle-back:focus-visible, .atlas-embed-step-back:focus-visible,\n.atlas-embed-password-toggle:focus-visible, .atlas-embed-carousel-nav:focus-visible {\n outline: 2px solid var(--_focus); outline-offset: 2px;\n}\n.atlas-embed-handle-back {\n width: 100%; margin-top: 8px; padding: 6px; font-size: 13px; font-weight: 550;\n cursor: pointer; color: #6b7280; background: none; border: 0;\n}\n.atlas-embed-handle-back:hover { color: #374151; }\n.atlas-embed-error { display: block; font-size: 13px; color: #d64545; margin: 6px 0 0; }\n.atlas-embed-form-error { font-size: 13px; color: #d64545; margin: 0 0 12px; min-height: 0; }\n/* A success/confirmation banner (e.g. after a force-login password reset). */\n.atlas-embed-flash {\n font-size: 13px; color: #0f7a4d; background: rgba(16,163,101,0.08);\n border: 1px solid rgba(16,163,101,0.25); border-radius: var(--_radius);\n padding: 9px 11px; margin: 0 0 14px; text-align: center;\n}\n.atlas-embed-footer { font-size: 13px; color: #6b7280; text-align: center; margin: 16px 0 0; }\n/* \u00A75.1 legal consent row on sign-up. */\n.atlas-embed-consent { display: flex; align-items: flex-start; gap: 8px; margin: 2px 0 14px; font-size: 13px; color: #6b7280; }\n.atlas-embed-consent-check { margin: 2px 0 0; width: 15px; height: 15px; flex: none; accent-color: var(--_primary); cursor: pointer; }\n.atlas-embed-consent-link { color: var(--_primary); text-decoration: underline; }\n/* \u00A77.5 Remember-me row. */\n.atlas-embed-remember { display: flex; align-items: center; gap: 8px; margin: -2px 0 14px; font-size: 13px; color: var(--_text); cursor: pointer; }\n.atlas-embed-remember-check { width: 15px; height: 15px; flex: none; accent-color: var(--_primary); cursor: pointer; }\n.atlas-embed-note { font-size: 14px; color: #6b7280; text-align: center; margin: 8px 0; }\n\n/* Verification step \u2014 a continuation of the email panel, not a new screen: the\n context line (\"Enter the code we sent to \u2026\") + a way back, then a segmented\n one-box-per-digit code input that spawns in. */\n.atlas-embed-step-context { text-align: center; margin: 0 0 20px; }\n.atlas-embed-step-context .atlas-embed-subtitle { margin: 0 0 6px; }\n.atlas-embed-step-back {\n display: inline-block; padding: 2px 4px; font: inherit; font-size: 13px; font-weight: 550;\n cursor: pointer; color: var(--_primary); background: none; border: 0;\n}\n.atlas-embed-step-back:hover { text-decoration: underline; }\n/* A standalone action under the submit button (Forgot password?, Back to sign in,\n the 2FA toggle) reads as a centred link, not a left-aligned fragment. */\nform > .atlas-embed-step-back { display: block; width: 100%; text-align: center; margin-top: 10px; }\n.atlas-embed-code { display: flex; gap: 8px; justify-content: space-between; }\n.atlas-embed-code-box {\n flex: 1 1 0; min-width: 0; aspect-ratio: 1 / 1.15; padding: 0; text-align: center;\n font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--_text);\n background: var(--_bg); border: 1px solid var(--_border); border-radius: var(--_radius);\n}\n.atlas-embed-code-box:focus { outline: 2px solid var(--_focus); outline-offset: -1px; }\n/* A real step change eases its content in; error/busy re-renders (same step) do\n not, and a reduced-motion viewer gets no animation at all. */\n@media (prefers-reduced-motion: no-preference) {\n .atlas-embed-card[data-enter] > form,\n .atlas-embed-card[data-enter] > .atlas-embed-step-context {\n animation: atlas-embed-spawn 0.26s cubic-bezier(0.22, 0.61, 0.36, 1) both;\n }\n}\n@keyframes atlas-embed-spawn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Interaction / polish layer \u2014 how controls FEEL. Driven by data-attributes on\n the root (data-button-style / -button-effect / -shadow / -density /\n -input-style) that the widget sets from the resolved interaction config, plus\n the --_motion tempo var. Every rule is additive: with no attributes set the\n widget looks exactly as it did before this layer existed.\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Smooth every interactive control at the configured tempo. */\n.atlas-embed-button, .atlas-embed-social-button, .atlas-embed-carousel-nav,\n.atlas-embed-handle-back, .atlas-embed-step-back, .atlas-embed-password-toggle,\n.atlas-embed-input, .atlas-embed-code-box {\n transition:\n transform var(--_motion) ease, box-shadow var(--_motion) ease,\n background-color var(--_motion) ease, border-color var(--_motion) ease,\n color var(--_motion) ease, filter var(--_motion) ease, outline-color var(--_motion) ease;\n}\n\n/* Primary CTA \u2014 a subtle hover darken by default (was static). */\n.atlas-embed-button:hover:not(:disabled) { filter: brightness(1.06); }\n\n/* Button fill style (primary CTA only \u2014 social buttons keep provider styling). */\n.atlas-embed-root[data-button-style=\"outline\"] .atlas-embed-button {\n background: transparent; color: var(--_primary); border: 1px solid var(--_primary);\n}\n.atlas-embed-root[data-button-style=\"outline\"] .atlas-embed-button:hover:not(:disabled) {\n filter: none; background: color-mix(in srgb, var(--_primary) 10%, transparent);\n}\n.atlas-embed-root[data-button-style=\"soft\"] .atlas-embed-button {\n background: color-mix(in srgb, var(--_primary) 14%, transparent); color: var(--_primary);\n}\n.atlas-embed-root[data-button-style=\"soft\"] .atlas-embed-button:hover:not(:disabled) {\n filter: none; background: color-mix(in srgb, var(--_primary) 22%, transparent);\n}\n.atlas-embed-root[data-button-style=\"ghost\"] .atlas-embed-button {\n background: transparent; color: var(--_primary);\n}\n.atlas-embed-root[data-button-style=\"ghost\"] .atlas-embed-button:hover:not(:disabled) {\n filter: none; background: color-mix(in srgb, var(--_primary) 12%, transparent);\n}\n\n/* Press/hover effect \u2014 applies to every button. */\n.atlas-embed-root[data-button-effect=\"scale\"] .atlas-embed-button:active:not(:disabled),\n.atlas-embed-root[data-button-effect=\"scale\"] .atlas-embed-social-button:active,\n.atlas-embed-root[data-button-effect=\"scale\"] .atlas-embed-carousel-nav:active:not(:disabled) {\n transform: scale(0.97);\n}\n.atlas-embed-root[data-button-effect=\"lift\"] .atlas-embed-button:hover:not(:disabled),\n.atlas-embed-root[data-button-effect=\"lift\"] .atlas-embed-social-button:hover {\n transform: translateY(-1px); box-shadow: 0 4px 14px rgba(0,0,0,0.14); filter: none;\n}\n.atlas-embed-root[data-button-effect=\"lift\"] .atlas-embed-button:active:not(:disabled),\n.atlas-embed-root[data-button-effect=\"lift\"] .atlas-embed-social-button:active { transform: translateY(0); }\n.atlas-embed-root[data-button-effect=\"glow\"] .atlas-embed-button:hover:not(:disabled),\n.atlas-embed-root[data-button-effect=\"glow\"] .atlas-embed-social-button:hover {\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--_primary) 35%, transparent); filter: none;\n}\n/* Ripple \u2014 the widget appends a span on pointer-down; buttons must clip it. */\n.atlas-embed-root[data-button-effect=\"ripple\"] .atlas-embed-button,\n.atlas-embed-root[data-button-effect=\"ripple\"] .atlas-embed-social-button {\n position: relative; overflow: hidden;\n}\n.atlas-embed-ripple {\n position: absolute; border-radius: 50%; transform: scale(0);\n background: currentColor; opacity: 0.28; pointer-events: none;\n animation: atlas-embed-ripple 0.55s ease-out;\n}\n@keyframes atlas-embed-ripple { to { transform: scale(2.6); opacity: 0; } }\n\n/* Card + solid-button elevation. */\n.atlas-embed-root[data-shadow=\"none\"] .atlas-embed-card { box-shadow: none; }\n.atlas-embed-root[data-shadow=\"sm\"] .atlas-embed-card { box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\n.atlas-embed-root[data-shadow=\"lg\"] .atlas-embed-card {\n box-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 16px 48px rgba(0,0,0,0.12);\n}\n\n/* Density \u2014 compact tightens padding and gaps. */\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-card { padding: 20px 18px; }\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-social { gap: 6px; margin-bottom: 12px; }\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-field { margin-bottom: 9px; }\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-button { padding: 9px 12px; }\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-social-button { padding: 8px 10px; }\n.atlas-embed-root[data-density=\"compact\"] .atlas-embed-subtitle { margin-bottom: 14px; }\n\n/* Input style \u2014 filled and underline treatments (default is the outlined field). */\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-input,\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-code-box,\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-social-search {\n background: color-mix(in srgb, var(--_border) 24%, transparent); border-color: transparent;\n}\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-input:focus,\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-code-box:focus,\n.atlas-embed-root[data-input-style=\"filled\"] .atlas-embed-social-search:focus {\n background: var(--_bg); border-color: var(--_primary);\n}\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-input,\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-code-box,\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-social-search {\n background: transparent; border: 0; border-bottom: 1.5px solid var(--_border);\n border-radius: 0; padding-left: 2px; padding-right: 2px;\n}\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-input:focus,\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-code-box:focus,\n.atlas-embed-root[data-input-style=\"underline\"] .atlas-embed-social-search:focus {\n outline: none; border-bottom-color: var(--_primary);\n}\n\n/* \u2500\u2500 Loading states \u2014 spinner / skeleton / dots / bar (interaction.loading).\n Shown while appearance resolves, while a redirect ticket is exchanged, and the\n button spinner while a form submits \u2014 so the widget never flashes blank. \u2500\u2500 */\n.atlas-embed-button { display: flex; align-items: center; justify-content: center; gap: 8px; }\n.atlas-embed-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; min-height: 168px; padding: 12px 0; }\n.atlas-embed-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }\n\n/* Spinner (default) + the in-button spinner. */\n.atlas-embed-spinner, .atlas-embed-btn-spinner {\n border-radius: 50%; border-style: solid; flex: none;\n animation: atlas-embed-spin 0.7s linear infinite;\n}\n.atlas-embed-spinner { width: 30px; height: 30px; border-width: 3px;\n border-color: color-mix(in srgb, var(--_primary) 22%, transparent); border-top-color: var(--_primary); }\n.atlas-embed-btn-spinner { width: 17px; height: 17px; border-width: 2px;\n border-color: color-mix(in srgb, currentColor 35%, transparent); border-top-color: currentColor; }\n@keyframes atlas-embed-spin { to { transform: rotate(360deg); } }\n\n/* Dots. */\n.atlas-embed-loading[data-loading=\"dots\"] { flex-direction: row; }\n.atlas-embed-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--_primary);\n animation: atlas-embed-bounce 1.2s ease-in-out infinite; }\n.atlas-embed-dot:nth-child(2) { animation-delay: 0.15s; }\n.atlas-embed-dot:nth-child(3) { animation-delay: 0.30s; }\n@keyframes atlas-embed-bounce { 0%,80%,100% { transform: scale(0.5); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }\n\n/* Indeterminate bar. */\n.atlas-embed-loading[data-loading=\"bar\"] { min-height: 132px; }\n.atlas-embed-loadbar { position: relative; width: 64%; height: 4px; border-radius: 999px; overflow: hidden;\n background: color-mix(in srgb, var(--_border) 60%, transparent); }\n.atlas-embed-loadbar::before { content: \"\"; position: absolute; top: 0; bottom: 0; width: 40%; border-radius: 999px;\n background: var(--_primary); animation: atlas-embed-slide 1.1s ease-in-out infinite; }\n@keyframes atlas-embed-slide { 0% { left: -45%; } 100% { left: 105%; } }\n\n/* Skeleton \u2014 placeholders shaped like the form. */\n.atlas-embed-loading[data-loading=\"skeleton\"] { align-items: stretch; min-height: 0; gap: 12px; }\n.atlas-embed-skel { border-radius: var(--_radius); position: relative; overflow: hidden;\n background: color-mix(in srgb, var(--_border) 55%, transparent); }\n.atlas-embed-skel::after { content: \"\"; position: absolute; inset: 0; transform: translateX(-100%);\n background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--_text) 8%, transparent), transparent);\n animation: atlas-embed-shimmer 1.3s infinite; }\n.atlas-embed-skel-title { height: 22px; width: 55%; margin: 2px auto 2px; }\n.atlas-embed-skel-sub { height: 13px; width: 75%; margin: 0 auto 8px; }\n.atlas-embed-skel-input { height: 42px; }\n.atlas-embed-skel-button { height: 44px; margin-top: 6px; }\n@keyframes atlas-embed-shimmer { to { transform: translateX(100%); } }\n\n/* Reduced motion always wins \u2014 no transitions, presses, ripples, or spawn; and\n loaders swap their spin/bounce/slide/shimmer for a minimal opacity pulse. */\n@media (prefers-reduced-motion: reduce) {\n .atlas-embed-root { --_motion: 0s; }\n .atlas-embed-root [class^=\"atlas-embed-\"] { transition: none !important; }\n .atlas-embed-root .atlas-embed-button:active,\n .atlas-embed-root .atlas-embed-social-button:active { transform: none !important; }\n .atlas-embed-ripple { display: none; }\n .atlas-embed-spinner, .atlas-embed-btn-spinner, .atlas-embed-dot,\n .atlas-embed-loadbar::before, .atlas-embed-skel::after {\n animation: atlas-embed-pulse 1.4s ease-in-out infinite !important;\n }\n .atlas-embed-loadbar::before { position: static; width: 100%; }\n}\n@keyframes atlas-embed-pulse { 0%,100% { opacity: 0.4; } 50% { opacity: 1; } }\n";
|
|
16
16
|
/** Inject the sheet once per document. Safe to call on every mount. */
|
|
17
17
|
export declare function injectStyles(doc?: Document): void;
|
package/dist/styles.js
CHANGED
|
@@ -22,6 +22,9 @@ exports.EMBED_CSS = `
|
|
|
22
22
|
--_card: var(--atlas-embed-color-card, #ffffff);
|
|
23
23
|
--_text: var(--atlas-embed-color-text, #1c1c1f);
|
|
24
24
|
--_primary: var(--atlas-embed-color-primary, #5b5bd6);
|
|
25
|
+
--_on-primary: var(--atlas-embed-color-text-on-primary, #ffffff);
|
|
26
|
+
/* Keyboard focus ring — a solid outline (3:1 on the default background). */
|
|
27
|
+
--_focus: var(--atlas-embed-color-focus-ring, var(--_primary));
|
|
25
28
|
--_border: var(--atlas-embed-color-border, #e4e4e9);
|
|
26
29
|
--_radius: var(--atlas-embed-border-radius, 10px);
|
|
27
30
|
/* Transition tempo — the widget sets --atlas-embed-motion from motionSpeed and
|
|
@@ -110,10 +113,10 @@ exports.EMBED_CSS = `
|
|
|
110
113
|
.atlas-embed-social-field { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
|
|
111
114
|
.atlas-embed-social-field .atlas-embed-social { margin-bottom: 0; }
|
|
112
115
|
.atlas-embed-social-search {
|
|
113
|
-
width: 100%; padding: 9px 12px; font-size:
|
|
116
|
+
width: 100%; padding: 9px 12px; font-size: 16px; color: var(--_text);
|
|
114
117
|
background: var(--_bg); border: 1px solid var(--_border); border-radius: var(--_radius);
|
|
115
118
|
}
|
|
116
|
-
.atlas-embed-social-search:focus { outline: 2px solid var(--
|
|
119
|
+
.atlas-embed-social-search:focus { outline: 2px solid var(--_focus); outline-offset: -1px; }
|
|
117
120
|
.atlas-embed-social-button[hidden] { display: none; }
|
|
118
121
|
.atlas-embed-social-empty { font-size: 13px; color: #9ca3af; text-align: center; padding: 6px 0; }
|
|
119
122
|
.atlas-embed-social-empty[hidden] { display: none; }
|
|
@@ -127,10 +130,11 @@ exports.EMBED_CSS = `
|
|
|
127
130
|
.atlas-embed-field { display: block; margin: 0 0 12px; }
|
|
128
131
|
.atlas-embed-label { display: block; font-size: 13px; margin: 0 0 5px; color: var(--_text); }
|
|
129
132
|
.atlas-embed-input {
|
|
130
|
-
|
|
133
|
+
/* 16px: iOS Safari zooms the page when a smaller field takes focus. */
|
|
134
|
+
width: 100%; padding: 10px 12px; font-size: 16px; color: var(--_text);
|
|
131
135
|
background: var(--_bg); border: 1px solid var(--_border); border-radius: var(--_radius);
|
|
132
136
|
}
|
|
133
|
-
.atlas-embed-input:focus { outline: 2px solid var(--
|
|
137
|
+
.atlas-embed-input:focus { outline: 2px solid var(--_focus); outline-offset: -1px; }
|
|
134
138
|
/* Password field with a show/hide toggle. */
|
|
135
139
|
.atlas-embed-password { position: relative; display: flex; align-items: center; }
|
|
136
140
|
.atlas-embed-password .atlas-embed-input { width: 100%; padding-right: 54px; }
|
|
@@ -150,10 +154,15 @@ fieldset.atlas-embed-field { border: 0; padding: 0; margin: 0 0 12px; }
|
|
|
150
154
|
.atlas-embed-choice input { width: 16px; height: 16px; margin: 0; flex: none; accent-color: var(--_primary); }
|
|
151
155
|
.atlas-embed-button {
|
|
152
156
|
width: 100%; margin-top: 6px; padding: 11px 12px; font-size: 14px; font-weight: 650;
|
|
153
|
-
cursor: pointer; color:
|
|
157
|
+
cursor: pointer; color: var(--_on-primary); background: var(--_primary);
|
|
154
158
|
border: 0; border-radius: var(--_radius);
|
|
155
159
|
}
|
|
156
160
|
.atlas-embed-button:disabled { opacity: 0.6; cursor: default; }
|
|
161
|
+
.atlas-embed-button:focus-visible, .atlas-embed-social-button:focus-visible,
|
|
162
|
+
.atlas-embed-handle-back:focus-visible, .atlas-embed-step-back:focus-visible,
|
|
163
|
+
.atlas-embed-password-toggle:focus-visible, .atlas-embed-carousel-nav:focus-visible {
|
|
164
|
+
outline: 2px solid var(--_focus); outline-offset: 2px;
|
|
165
|
+
}
|
|
157
166
|
.atlas-embed-handle-back {
|
|
158
167
|
width: 100%; margin-top: 8px; padding: 6px; font-size: 13px; font-weight: 550;
|
|
159
168
|
cursor: pointer; color: #6b7280; background: none; border: 0;
|
|
@@ -196,7 +205,7 @@ form > .atlas-embed-step-back { display: block; width: 100%; text-align: center;
|
|
|
196
205
|
font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--_text);
|
|
197
206
|
background: var(--_bg); border: 1px solid var(--_border); border-radius: var(--_radius);
|
|
198
207
|
}
|
|
199
|
-
.atlas-embed-code-box:focus { outline: 2px solid var(--
|
|
208
|
+
.atlas-embed-code-box:focus { outline: 2px solid var(--_focus); outline-offset: -1px; }
|
|
200
209
|
/* A real step change eases its content in; error/busy re-renders (same step) do
|
|
201
210
|
not, and a reduced-motion viewer gets no animation at all. */
|
|
202
211
|
@media (prefers-reduced-motion: no-preference) {
|
package/dist/styles.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"styles.js","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"styles.js","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":";;;AAmYA,oCAMC;AAzYD;;;;;;;;;;;;GAYG;AACU,QAAA,YAAY,GAAG,oBAAoB,CAAC;AAEpC,QAAA,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiXxB,CAAC;AAEF,uEAAuE;AACvE,SAAgB,YAAY,CAAC,MAAgB,QAAQ;IACnD,IAAI,GAAG,CAAC,aAAa,CAAC,cAAc,oBAAY,GAAG,CAAC;QAAE,OAAO;IAC7D,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IACzC,KAAK,CAAC,YAAY,CAAC,QAAQ,oBAAY,EAAE,EAAE,EAAE,CAAC,CAAC;IAC/C,KAAK,CAAC,WAAW,GAAG,iBAAS,CAAC;IAC9B,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,eAAe,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AACvD,CAAC"}
|
package/dist/widget/captcha.d.ts
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import type { FlowCaptcha } from '../appearance';
|
|
2
|
-
export declare function isRenderable(provider?: string | null): boolean;
|
|
3
|
-
/** True when this flow needs a rendered token before its request can proceed. */
|
|
4
|
-
export declare function flowNeedsCaptcha(flow: FlowCaptcha | undefined): flow is FlowCaptcha;
|
|
5
|
-
type Win = Window & Record<string, unknown>;
|
|
6
1
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* Obtain a captcha token for a flow. Loads the provider, renders/executes the
|
|
14
|
-
* right widget for the mode, and resolves the token. `container` is where a
|
|
15
|
-
* visible/managed widget draws; for score providers it is unused.
|
|
2
|
+
* Client-side captcha rendering for the hosted widget.
|
|
3
|
+
*
|
|
4
|
+
* The server tells the widget, per flow, which provider to render and in what
|
|
5
|
+
* mode (see `/v1/appearance` → `captchaFlows`). The loader/renderer lives in
|
|
6
|
+
* `@atlasauth/js` so the embed widget and the React components draw the same
|
|
7
|
+
* widget the same way; this module keeps the widget's historical names.
|
|
16
8
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
9
|
+
* Only the providers that expose a standard browser widget are renderable here;
|
|
10
|
+
* score/edge/fingerprint providers are verified server-side from a token a
|
|
11
|
+
* custom integration supplies, so `isRenderable` returns false for them and the
|
|
12
|
+
* caller proceeds without a token (the server still fails closed if it required
|
|
13
|
+
* one).
|
|
19
14
|
*/
|
|
20
|
-
export
|
|
21
|
-
export {};
|
|
15
|
+
export { isRenderableCaptcha as isRenderable, flowNeedsCaptcha, loadCaptchaProvider as loadProvider, getCaptchaToken, } from '@atlasauth/js';
|
package/dist/widget/captcha.js
CHANGED
|
@@ -1,197 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isRenderable =
|
|
4
|
-
exports.flowNeedsCaptcha = flowNeedsCaptcha;
|
|
5
|
-
exports.loadProvider = loadProvider;
|
|
6
|
-
exports.getCaptchaToken = getCaptchaToken;
|
|
3
|
+
exports.getCaptchaToken = exports.loadProvider = exports.flowNeedsCaptcha = exports.isRenderable = void 0;
|
|
7
4
|
/**
|
|
8
5
|
* Client-side captcha rendering for the hosted widget.
|
|
9
6
|
*
|
|
10
7
|
* The server tells the widget, per flow, which provider to render and in what
|
|
11
|
-
* mode (see `/v1/appearance` → `captchaFlows`).
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* are renderable here; score/edge/fingerprint providers (SEON, PerimeterX,
|
|
15
|
-
* Akamai, Kasada, Incapsula, mCaptcha, AWS WAF, …) are verified server-side from
|
|
16
|
-
* a token a custom integration supplies, so the hosted widget does not draw
|
|
17
|
-
* them — `isRenderable` returns false and the caller proceeds without a token
|
|
18
|
-
* (the server still fails closed if it required one).
|
|
19
|
-
*/
|
|
20
|
-
/** The providers the hosted widget can natively render. */
|
|
21
|
-
const RENDERABLE = new Set([
|
|
22
|
-
'turnstile',
|
|
23
|
-
'hcaptcha',
|
|
24
|
-
'recaptcha',
|
|
25
|
-
'recaptcha_v3',
|
|
26
|
-
'recaptcha_enterprise',
|
|
27
|
-
]);
|
|
28
|
-
function isRenderable(provider) {
|
|
29
|
-
return !!provider && RENDERABLE.has(provider);
|
|
30
|
-
}
|
|
31
|
-
/** True when this flow needs a rendered token before its request can proceed. */
|
|
32
|
-
function flowNeedsCaptcha(flow) {
|
|
33
|
-
return !!flow && flow.enabled === true && isRenderable(flow.provider);
|
|
34
|
-
}
|
|
35
|
-
const SCRIPTS = {
|
|
36
|
-
turnstile: {
|
|
37
|
-
src: () => 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit',
|
|
38
|
-
global: 'turnstile',
|
|
39
|
-
},
|
|
40
|
-
hcaptcha: {
|
|
41
|
-
src: () => 'https://js.hcaptcha.com/1/api.js?render=explicit',
|
|
42
|
-
global: 'hcaptcha',
|
|
43
|
-
},
|
|
44
|
-
recaptcha: {
|
|
45
|
-
src: () => 'https://www.google.com/recaptcha/api.js?render=explicit',
|
|
46
|
-
global: 'grecaptcha',
|
|
47
|
-
},
|
|
48
|
-
recaptcha_v3: {
|
|
49
|
-
src: (siteKey) => `https://www.google.com/recaptcha/api.js?render=${encodeURIComponent(siteKey)}`,
|
|
50
|
-
global: 'grecaptcha',
|
|
51
|
-
},
|
|
52
|
-
recaptcha_enterprise: {
|
|
53
|
-
src: (siteKey) => `https://www.google.com/recaptcha/enterprise.js?render=${encodeURIComponent(siteKey)}`,
|
|
54
|
-
global: 'grecaptcha',
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
/**
|
|
58
|
-
* Load a provider's script and resolve its global. Idempotent: a second call for
|
|
59
|
-
* the same provider reuses the in-flight or resolved load. When the global is
|
|
60
|
-
* already present (a prior load, or a test that preset it) it resolves at once.
|
|
61
|
-
*/
|
|
62
|
-
function loadProvider(provider, siteKey, win) {
|
|
63
|
-
const spec = SCRIPTS[provider];
|
|
64
|
-
if (!spec)
|
|
65
|
-
return Promise.reject(new Error(`captcha: ${provider} is not renderable`));
|
|
66
|
-
if (win[spec.global])
|
|
67
|
-
return Promise.resolve(win[spec.global]);
|
|
68
|
-
const doc = win.document;
|
|
69
|
-
const cacheKey = `__atlasCaptchaLoad_${provider}`;
|
|
70
|
-
const existing = win[cacheKey];
|
|
71
|
-
if (existing)
|
|
72
|
-
return existing;
|
|
73
|
-
const promise = new Promise((resolve, reject) => {
|
|
74
|
-
const done = () => {
|
|
75
|
-
if (win[spec.global])
|
|
76
|
-
resolve(win[spec.global]);
|
|
77
|
-
else
|
|
78
|
-
reject(new Error(`captcha: ${provider} script loaded but global missing`));
|
|
79
|
-
};
|
|
80
|
-
const src = spec.src(siteKey);
|
|
81
|
-
// Reuse a script tag another flow may have injected for the same src.
|
|
82
|
-
let el = doc.querySelector(`script[src="${src}"]`);
|
|
83
|
-
if (el) {
|
|
84
|
-
if (win[spec.global])
|
|
85
|
-
return resolve(win[spec.global]);
|
|
86
|
-
el.addEventListener('load', done, { once: true });
|
|
87
|
-
el.addEventListener('error', () => reject(new Error(`captcha: ${provider} script failed`)), {
|
|
88
|
-
once: true,
|
|
89
|
-
});
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
el = doc.createElement('script');
|
|
93
|
-
el.src = src;
|
|
94
|
-
el.async = true;
|
|
95
|
-
el.defer = true;
|
|
96
|
-
el.addEventListener('load', done, { once: true });
|
|
97
|
-
el.addEventListener('error', () => reject(new Error(`captcha: ${provider} script failed`)), {
|
|
98
|
-
once: true,
|
|
99
|
-
});
|
|
100
|
-
(doc.head ?? doc.body ?? doc.documentElement).appendChild(el);
|
|
101
|
-
});
|
|
102
|
-
win[cacheKey] = promise;
|
|
103
|
-
return promise;
|
|
104
|
-
}
|
|
105
|
-
/** Cloudflare Turnstile: render into the container, resolve on the callback. */
|
|
106
|
-
function turnstileToken(api, el, siteKey, mode) {
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
const id = api.render(el, {
|
|
109
|
-
sitekey: siteKey,
|
|
110
|
-
// Turnstile's own modes map cleanly onto ours.
|
|
111
|
-
appearance: mode === 'invisible' ? 'interaction-only' : 'always',
|
|
112
|
-
callback: (token) => resolve(token),
|
|
113
|
-
'error-callback': () => reject(new Error('captcha: turnstile error')),
|
|
114
|
-
'timeout-callback': () => reject(new Error('captcha: turnstile timeout')),
|
|
115
|
-
});
|
|
116
|
-
if (mode === 'invisible' && api.execute)
|
|
117
|
-
api.execute(id);
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
/** hCaptcha: render, then execute for invisible; resolve on the callback. */
|
|
121
|
-
function hcaptchaToken(api, el, siteKey, mode) {
|
|
122
|
-
return new Promise((resolve, reject) => {
|
|
123
|
-
const id = api.render(el, {
|
|
124
|
-
sitekey: siteKey,
|
|
125
|
-
size: mode === 'invisible' ? 'invisible' : 'normal',
|
|
126
|
-
callback: (token) => resolve(token),
|
|
127
|
-
'error-callback': () => reject(new Error('captcha: hcaptcha error')),
|
|
128
|
-
});
|
|
129
|
-
if (mode === 'invisible') {
|
|
130
|
-
const r = api.execute(id, { async: true });
|
|
131
|
-
if (r && typeof r.then === 'function') {
|
|
132
|
-
r.then((v) => resolve(v.response), reject);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
/** reCAPTCHA v2 (checkbox/invisible): render, resolve on callback. */
|
|
138
|
-
function recaptchaV2Token(api, el, siteKey, mode) {
|
|
139
|
-
return new Promise((resolve, reject) => {
|
|
140
|
-
const id = api.render(el, {
|
|
141
|
-
sitekey: siteKey,
|
|
142
|
-
size: mode === 'invisible' ? 'invisible' : 'normal',
|
|
143
|
-
callback: (token) => resolve(token),
|
|
144
|
-
'error-callback': () => reject(new Error('captcha: recaptcha error')),
|
|
145
|
-
});
|
|
146
|
-
if (mode === 'invisible' && api.execute) {
|
|
147
|
-
const r = api.execute(undefined, { widgetId: id });
|
|
148
|
-
if (r && typeof r.then === 'function') {
|
|
149
|
-
r.then(resolve, reject);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
/** reCAPTCHA v3 / Enterprise (score): execute programmatically, no visible widget. */
|
|
155
|
-
function recaptchaV3Token(api, siteKey) {
|
|
156
|
-
const run = () => {
|
|
157
|
-
const r = api.execute(siteKey, { action: 'submit' });
|
|
158
|
-
return r && typeof r.then === 'function'
|
|
159
|
-
? r
|
|
160
|
-
: Promise.reject(new Error('captcha: recaptcha v3 execute returned no promise'));
|
|
161
|
-
};
|
|
162
|
-
return new Promise((resolve, reject) => {
|
|
163
|
-
if (api.ready)
|
|
164
|
-
api.ready(() => run().then(resolve, reject));
|
|
165
|
-
else
|
|
166
|
-
run().then(resolve, reject);
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Obtain a captcha token for a flow. Loads the provider, renders/executes the
|
|
171
|
-
* right widget for the mode, and resolves the token. `container` is where a
|
|
172
|
-
* visible/managed widget draws; for score providers it is unused.
|
|
8
|
+
* mode (see `/v1/appearance` → `captchaFlows`). The loader/renderer lives in
|
|
9
|
+
* `@atlasauth/js` so the embed widget and the React components draw the same
|
|
10
|
+
* widget the same way; this module keeps the widget's historical names.
|
|
173
11
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
12
|
+
* Only the providers that expose a standard browser widget are renderable here;
|
|
13
|
+
* score/edge/fingerprint providers are verified server-side from a token a
|
|
14
|
+
* custom integration supplies, so `isRenderable` returns false for them and the
|
|
15
|
+
* caller proceeds without a token (the server still fails closed if it required
|
|
16
|
+
* one).
|
|
176
17
|
*/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const api = await loadProvider(provider, siteKey, win);
|
|
183
|
-
switch (provider) {
|
|
184
|
-
case 'turnstile':
|
|
185
|
-
return turnstileToken(api, container, siteKey, mode);
|
|
186
|
-
case 'hcaptcha':
|
|
187
|
-
return hcaptchaToken(api, container, siteKey, mode);
|
|
188
|
-
case 'recaptcha':
|
|
189
|
-
return recaptchaV2Token(api, container, siteKey, mode);
|
|
190
|
-
case 'recaptcha_v3':
|
|
191
|
-
case 'recaptcha_enterprise':
|
|
192
|
-
return recaptchaV3Token(api, siteKey);
|
|
193
|
-
default:
|
|
194
|
-
throw new Error(`captcha: ${provider} is not renderable`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
18
|
+
var js_1 = require("@atlasauth/js");
|
|
19
|
+
Object.defineProperty(exports, "isRenderable", { enumerable: true, get: function () { return js_1.isRenderableCaptcha; } });
|
|
20
|
+
Object.defineProperty(exports, "flowNeedsCaptcha", { enumerable: true, get: function () { return js_1.flowNeedsCaptcha; } });
|
|
21
|
+
Object.defineProperty(exports, "loadProvider", { enumerable: true, get: function () { return js_1.loadCaptchaProvider; } });
|
|
22
|
+
Object.defineProperty(exports, "getCaptchaToken", { enumerable: true, get: function () { return js_1.getCaptchaToken; } });
|
|
197
23
|
//# sourceMappingURL=captcha.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"captcha.js","sourceRoot":"","sources":["../../src/widget/captcha.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"captcha.js","sourceRoot":"","sources":["../../src/widget/captcha.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;GAaG;AACH,oCAKuB;AAJrB,kGAAA,mBAAmB,OAAgB;AACnC,sGAAA,gBAAgB,OAAA;AAChB,kGAAA,mBAAmB,OAAgB;AACnC,qGAAA,eAAe,OAAA"}
|
package/dist/widget/forms.d.ts
CHANGED
|
@@ -32,4 +32,6 @@ export declare function renderLegalConsent(w: AtlasWidget): {
|
|
|
32
32
|
* /v1/client/sign_ups; the server then emails a code and the shared flow drives
|
|
33
33
|
* verification to completion.
|
|
34
34
|
*/
|
|
35
|
+
/** The end-user words the API uses for EMAIL_DELIVERY_NOT_CONFIGURED. */
|
|
36
|
+
export declare const EMAIL_UNAVAILABLE_MESSAGE = "This app can't send email right now. Try another sign-in method or contact the app owner.";
|
|
35
37
|
export declare function renderSignUpForm(w: AtlasWidget): HTMLElement;
|
package/dist/widget/forms.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMAIL_UNAVAILABLE_MESSAGE = void 0;
|
|
3
4
|
exports.renderReset = renderReset;
|
|
4
5
|
exports.fieldInto = fieldInto;
|
|
5
6
|
exports.codeFieldInto = codeFieldInto;
|
|
@@ -301,10 +302,10 @@ function renderForm(w, stepKind) {
|
|
|
301
302
|
}
|
|
302
303
|
// §5.4 "Forgot password?" — start the reset sub-flow. Shown wherever a password
|
|
303
304
|
// is being asked for (the single-screen box, or a dedicated password step), and
|
|
304
|
-
// only when password sign-in is actually on.
|
|
305
|
+
// only when password sign-in is actually on and the app can email the reset code.
|
|
305
306
|
const passwordStep = (stepKind === 'collect_identifier' && w.mode === 'sign-in' && w.passwordEnabled) ||
|
|
306
307
|
stepKind === 'collect_first_factor';
|
|
307
|
-
if (passwordStep) {
|
|
308
|
+
if (passwordStep && w.passwordResetEnabled) {
|
|
308
309
|
const forgot = (0, dom_1.el)(w, 'button', 'atlas-embed-step-back', 'Forgot password?');
|
|
309
310
|
forgot.type = 'button';
|
|
310
311
|
forgot.addEventListener('click', () => (0, flow_1.startReset)(w));
|
|
@@ -374,8 +375,16 @@ function renderLegalConsent(w) {
|
|
|
374
375
|
* /v1/client/sign_ups; the server then emails a code and the shared flow drives
|
|
375
376
|
* verification to completion.
|
|
376
377
|
*/
|
|
378
|
+
/** The end-user words the API uses for EMAIL_DELIVERY_NOT_CONFIGURED. */
|
|
379
|
+
exports.EMAIL_UNAVAILABLE_MESSAGE = "This app can't send email right now. Try another sign-in method or contact the app owner.";
|
|
377
380
|
function renderSignUpForm(w) {
|
|
378
381
|
const form = (0, dom_1.doc)(w).createElement('form');
|
|
382
|
+
// A sign-up that must verify by email on an app that can't send email would
|
|
383
|
+
// only be refused: say so instead of drawing the form.
|
|
384
|
+
if (!w.passwordSignUpEnabled) {
|
|
385
|
+
form.appendChild((0, dom_1.el)(w, 'div', 'atlas-embed-form-error', exports.EMAIL_UNAVAILABLE_MESSAGE));
|
|
386
|
+
return form;
|
|
387
|
+
}
|
|
379
388
|
const formError = w.state.errors.find((e) => !e.param);
|
|
380
389
|
if (formError)
|
|
381
390
|
form.appendChild((0, dom_1.el)(w, 'div', 'atlas-embed-form-error', formError.message));
|