@authhero/widget 0.38.7 → 0.38.9
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/dist/authhero-widget/authhero-widget.esm.js +1 -1
- package/dist/authhero-widget/index.esm.js +1 -1
- package/dist/authhero-widget/{p-f94037cd.entry.js → p-74f95441.entry.js} +1 -1
- package/dist/cjs/authhero-widget.cjs.entry.js +1 -1
- package/dist/cjs/index.cjs.js +1 -1
- package/dist/collection/components/authhero-widget/authhero-widget.css +41 -9
- package/dist/collection/page-chrome/chrome.js +165 -0
- package/dist/collection/page-chrome/colors.js +94 -0
- package/dist/collection/page-chrome/css.js +523 -0
- package/dist/collection/page-chrome/html.js +121 -0
- package/dist/collection/page-chrome/index.js +13 -0
- package/dist/collection/page-chrome/types.js +1 -0
- package/dist/components/authhero-widget.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/esm/authhero-widget.entry.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/types/page-chrome/chrome.d.ts +73 -0
- package/dist/types/page-chrome/colors.d.ts +10 -0
- package/dist/types/page-chrome/css.d.ts +42 -0
- package/dist/types/page-chrome/html.d.ts +55 -0
- package/dist/types/page-chrome/index.d.ts +13 -0
- package/dist/types/page-chrome/types.d.ts +23 -0
- package/hydrate/index.js +1 -1
- package/hydrate/index.mjs +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page CSS and body layout for the login page.
|
|
3
|
+
*
|
|
4
|
+
* The single source of truth for the chrome around the widget card: chip
|
|
5
|
+
* tokens, floating corner chips, the phone footer bar, background tint, view
|
|
6
|
+
* transitions and the responsive breakpoints. Both the authhero login page
|
|
7
|
+
* and the widget demo server render from this, so a change to the phone
|
|
8
|
+
* layout can't show up in one and not the other.
|
|
9
|
+
*/
|
|
10
|
+
import { darkModeCssVarRules } from "./colors";
|
|
11
|
+
import { buildThemePageBackground } from "./html";
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the page-body layout (centering, background, font) from the theme
|
|
14
|
+
* and branding. Keeps the inline `<body>` style (fragment path) and the
|
|
15
|
+
* `auth0:head` stylesheet rule (full-document path) in sync so an Auth0-style
|
|
16
|
+
* template centers on the page background just like the default chrome does.
|
|
17
|
+
*
|
|
18
|
+
* The "left"/"right" offsets below are the wide-viewport values; the mobile
|
|
19
|
+
* block in `buildPageCss` collapses them back to centered under 768px.
|
|
20
|
+
*/
|
|
21
|
+
export function buildBodyLayout(opts) {
|
|
22
|
+
const pageLayout = opts.themePageBackground?.page_layout || "center";
|
|
23
|
+
const justifyContent = pageLayout === "left"
|
|
24
|
+
? "flex-start"
|
|
25
|
+
: pageLayout === "right"
|
|
26
|
+
? "flex-end"
|
|
27
|
+
: "center";
|
|
28
|
+
const padding = pageLayout === "left"
|
|
29
|
+
? "20px 20px 20px 80px"
|
|
30
|
+
: pageLayout === "right"
|
|
31
|
+
? "20px 80px 20px 20px"
|
|
32
|
+
: "20px";
|
|
33
|
+
return {
|
|
34
|
+
background: buildThemePageBackground(opts.themePageBackground, opts.brandingPageBackground),
|
|
35
|
+
fontFamily: opts.fontUrl
|
|
36
|
+
? "'Inter', system-ui, sans-serif"
|
|
37
|
+
: "system-ui, -apple-system, sans-serif",
|
|
38
|
+
justifyContent,
|
|
39
|
+
padding,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function buildPageCss(opts) {
|
|
43
|
+
const { primaryColor, themePrimary, widgetBackground, bodyLayout, hasBgImage, } = opts;
|
|
44
|
+
const bodyRule = bodyLayout
|
|
45
|
+
? `
|
|
46
|
+
body {
|
|
47
|
+
min-height: 100vh;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: ${bodyLayout.justifyContent};
|
|
51
|
+
background: ${bodyLayout.background};
|
|
52
|
+
font-family: ${bodyLayout.fontFamily};
|
|
53
|
+
padding: ${bodyLayout.padding};
|
|
54
|
+
}`
|
|
55
|
+
: "";
|
|
56
|
+
return `
|
|
57
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
58
|
+
${bodyRule}
|
|
59
|
+
|
|
60
|
+
/* ============= STEP TRANSITIONS =============
|
|
61
|
+
Cross-document view transitions morph the widget's box between
|
|
62
|
+
login steps (e.g. when the next step is taller) and cross-fade the
|
|
63
|
+
form content, à la Stripe's dashboard login. This is opt-in per
|
|
64
|
+
same-origin navigation and a no-op (instant nav) on browsers that
|
|
65
|
+
don't support it yet. The widget is lifted into its own named group
|
|
66
|
+
so only it animates its size; the rest of the page (background +
|
|
67
|
+
chips) cross-fades via the default \`root\` group, which is invisible
|
|
68
|
+
since those are unchanged between steps. */
|
|
69
|
+
@view-transition { navigation: auto; }
|
|
70
|
+
|
|
71
|
+
/* Resize-forward (Stripe-style): the widget box morphs its height from
|
|
72
|
+
the old step to the new one — that resize is the main motion. Content
|
|
73
|
+
keeps its natural height (height: auto) so the snapshot isn't stretched
|
|
74
|
+
to the morphing box, and does a quick, clean cross-fade underneath. */
|
|
75
|
+
::view-transition-group(ah-widget) {
|
|
76
|
+
animation-duration: 420ms;
|
|
77
|
+
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
78
|
+
}
|
|
79
|
+
::view-transition-old(ah-widget),
|
|
80
|
+
::view-transition-new(ah-widget) {
|
|
81
|
+
height: auto;
|
|
82
|
+
}
|
|
83
|
+
::view-transition-old(ah-widget) {
|
|
84
|
+
animation: 140ms ease both ah-widget-out;
|
|
85
|
+
}
|
|
86
|
+
::view-transition-new(ah-widget) {
|
|
87
|
+
animation: 240ms ease 110ms both ah-widget-in;
|
|
88
|
+
}
|
|
89
|
+
@keyframes ah-widget-out { to { opacity: 0; } }
|
|
90
|
+
@keyframes ah-widget-in { from { opacity: 0; } }
|
|
91
|
+
|
|
92
|
+
@media (prefers-reduced-motion: reduce) {
|
|
93
|
+
::view-transition-group(*),
|
|
94
|
+
::view-transition-old(*),
|
|
95
|
+
::view-transition-new(*) { animation: none !important; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ============= CHROME TOKENS =============
|
|
99
|
+
The chip surface tokens flip based on:
|
|
100
|
+
- data-mode (light/dark) — controls fg/bg pair
|
|
101
|
+
- data-bg (image/none) — toggles whether chips have a surface
|
|
102
|
+
This keeps a single chip ruleset for all four combinations. */
|
|
103
|
+
:root {
|
|
104
|
+
/* Pill surface values live in their own *-pill vars so they survive the
|
|
105
|
+
data-bg="none" reset below; the base tokens reference them, and the
|
|
106
|
+
.ah-chip--pill modifier re-points the base tokens back at them to
|
|
107
|
+
force a pill even on a solid background. */
|
|
108
|
+
--ah-chip-bg-pill: rgba(15,17,21,0.55);
|
|
109
|
+
--ah-chip-bg-hover-pill: rgba(15,17,21,0.75);
|
|
110
|
+
--ah-chip-border-pill: rgba(255,255,255,0.12);
|
|
111
|
+
--ah-chip-logo-bg-pill: rgba(15,17,21,0.4);
|
|
112
|
+
--ah-chip-bg: var(--ah-chip-bg-pill);
|
|
113
|
+
--ah-chip-bg-hover: var(--ah-chip-bg-hover-pill);
|
|
114
|
+
--ah-chip-border: var(--ah-chip-border-pill);
|
|
115
|
+
--ah-chip-fg: rgba(255,255,255,0.85);
|
|
116
|
+
--ah-chip-fg-dim: rgba(255,255,255,0.6);
|
|
117
|
+
--ah-chip-fg-mid: rgba(255,255,255,0.7);
|
|
118
|
+
--ah-chip-fg-strong: rgba(255,255,255,0.95);
|
|
119
|
+
--ah-chip-active-bg: rgba(255,255,255,0.14);
|
|
120
|
+
--ah-chip-logo-bg: var(--ah-chip-logo-bg-pill);
|
|
121
|
+
--ah-legal-fg: rgba(255,255,255,0.55);
|
|
122
|
+
--ah-legal-fg-hover: rgba(255,255,255,0.95);
|
|
123
|
+
--ah-legal-sep: rgba(255,255,255,0.25);
|
|
124
|
+
/* Phone footer bar. The surface follows data-bg (a translucent slab
|
|
125
|
+
over a background image, nothing over a solid one); the foreground
|
|
126
|
+
and hairline follow light/dark like every other chrome token. */
|
|
127
|
+
--ah-footer-bg: var(--ah-chip-bg-pill);
|
|
128
|
+
--ah-footer-border: rgba(255,255,255,0.12);
|
|
129
|
+
--ah-footer-fg: rgba(255,255,255,0.70);
|
|
130
|
+
--ah-footer-fg-strong: rgba(255,255,255,0.95);
|
|
131
|
+
--ah-bg-tint: radial-gradient(
|
|
132
|
+
ellipse at center,
|
|
133
|
+
rgba(15,23,48,0.30) 0%,
|
|
134
|
+
rgba(15,23,48,0.55) 70%,
|
|
135
|
+
rgba(10,15,30,0.78) 100%);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Light page mode — flip to dark text on translucent white chips.
|
|
139
|
+
The :root block above seeds dark tokens; data-mode="light" overrides
|
|
140
|
+
them when the toggle is explicit, and the prefers-color-scheme block
|
|
141
|
+
below mirrors them when the user is in auto mode (no data-mode set). */
|
|
142
|
+
html[data-mode="light"] {
|
|
143
|
+
--ah-chip-bg-pill: rgba(255,255,255,0.7);
|
|
144
|
+
--ah-chip-bg-hover-pill: rgba(255,255,255,0.92);
|
|
145
|
+
--ah-chip-border-pill: rgba(15,17,21,0.08);
|
|
146
|
+
--ah-chip-logo-bg-pill: rgba(255,255,255,0.75);
|
|
147
|
+
--ah-chip-fg: #0f1115;
|
|
148
|
+
--ah-chip-fg-dim: rgba(15,17,21,0.55);
|
|
149
|
+
--ah-chip-fg-mid: rgba(15,17,21,0.65);
|
|
150
|
+
--ah-chip-fg-strong: rgba(15,17,21,0.95);
|
|
151
|
+
--ah-chip-active-bg: rgba(15,17,21,0.08);
|
|
152
|
+
--ah-legal-fg: rgba(15,17,21,0.5);
|
|
153
|
+
--ah-legal-fg-hover: rgba(15,17,21,0.9);
|
|
154
|
+
--ah-legal-sep: rgba(15,17,21,0.2);
|
|
155
|
+
--ah-footer-border: rgba(15,17,21,0.10);
|
|
156
|
+
--ah-footer-fg: rgba(15,17,21,0.60);
|
|
157
|
+
--ah-footer-fg-strong: rgba(15,17,21,0.95);
|
|
158
|
+
--ah-bg-tint: transparent;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@media (prefers-color-scheme: light) {
|
|
162
|
+
html:not([data-mode]) {
|
|
163
|
+
--ah-chip-bg-pill: rgba(255,255,255,0.7);
|
|
164
|
+
--ah-chip-bg-hover-pill: rgba(255,255,255,0.92);
|
|
165
|
+
--ah-chip-border-pill: rgba(15,17,21,0.08);
|
|
166
|
+
--ah-chip-logo-bg-pill: rgba(255,255,255,0.75);
|
|
167
|
+
--ah-chip-fg: #0f1115;
|
|
168
|
+
--ah-chip-fg-dim: rgba(15,17,21,0.55);
|
|
169
|
+
--ah-chip-fg-mid: rgba(15,17,21,0.65);
|
|
170
|
+
--ah-chip-fg-strong: rgba(15,17,21,0.95);
|
|
171
|
+
--ah-chip-active-bg: rgba(15,17,21,0.08);
|
|
172
|
+
--ah-legal-fg: rgba(15,17,21,0.5);
|
|
173
|
+
--ah-legal-fg-hover: rgba(15,17,21,0.9);
|
|
174
|
+
--ah-legal-sep: rgba(15,17,21,0.2);
|
|
175
|
+
--ah-footer-border: rgba(15,17,21,0.10);
|
|
176
|
+
--ah-footer-fg: rgba(15,17,21,0.60);
|
|
177
|
+
--ah-footer-fg-strong: rgba(15,17,21,0.95);
|
|
178
|
+
--ah-bg-tint: transparent;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* No background image — chips become text-only on a solid page color */
|
|
183
|
+
html[data-bg="none"] {
|
|
184
|
+
--ah-chip-bg: transparent;
|
|
185
|
+
--ah-chip-bg-hover: transparent;
|
|
186
|
+
--ah-chip-border: transparent;
|
|
187
|
+
--ah-chip-logo-bg: transparent;
|
|
188
|
+
/* On a solid page the phone footer sits directly on the widget's own
|
|
189
|
+
surface, so a translucent slab would read as a seam. Hairline only. */
|
|
190
|
+
--ah-footer-bg: transparent;
|
|
191
|
+
--ah-bg-tint: transparent;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* ============= BACKGROUND TINT =============
|
|
195
|
+
Only renders when bg image is present. The token is transparent
|
|
196
|
+
in data-bg=none mode so the element stays in the DOM but invisible. */
|
|
197
|
+
.ah-bg-tint {
|
|
198
|
+
position: fixed; inset: 0; z-index: 0; pointer-events: none;
|
|
199
|
+
background: var(--ah-bg-tint);
|
|
200
|
+
transition: background 300ms ease;
|
|
201
|
+
}
|
|
202
|
+
.widget-container {
|
|
203
|
+
position: relative;
|
|
204
|
+
z-index: 1;
|
|
205
|
+
/* Names this box as its own view-transition group so its size morphs
|
|
206
|
+
smoothly across step navigations (see STEP TRANSITIONS above). */
|
|
207
|
+
view-transition-name: ah-widget;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* ============= IN-FLOW WIDGET STACK =============
|
|
211
|
+
Optional wrapper (used by the default custom template) that places
|
|
212
|
+
in-flow content directly above/below the widget card, sharing its
|
|
213
|
+
width and centering. Unlike the fixed-position corner chips, these
|
|
214
|
+
regions are normal document flow — tenants author content into them
|
|
215
|
+
in their template. Empty regions collapse so they add no spacing. */
|
|
216
|
+
.ah-widget-stack {
|
|
217
|
+
position: relative;
|
|
218
|
+
z-index: 1;
|
|
219
|
+
display: flex;
|
|
220
|
+
flex-direction: column;
|
|
221
|
+
align-items: center;
|
|
222
|
+
gap: 16px;
|
|
223
|
+
width: clamp(320px, 100%, 400px);
|
|
224
|
+
}
|
|
225
|
+
.ah-widget-stack .widget-container { width: 100%; }
|
|
226
|
+
.ah-above-widget, .ah-below-widget {
|
|
227
|
+
width: 100%;
|
|
228
|
+
text-align: center;
|
|
229
|
+
color: var(--ah-chip-fg);
|
|
230
|
+
font-size: 13px;
|
|
231
|
+
line-height: 1.5;
|
|
232
|
+
}
|
|
233
|
+
.ah-above-widget:empty, .ah-below-widget:empty { display: none; }
|
|
234
|
+
.ah-above-widget a, .ah-below-widget a { color: var(--ah-color-link, #2563eb); }
|
|
235
|
+
|
|
236
|
+
/* The "widget" logo position is rendered by the widget's own shadow DOM
|
|
237
|
+
(see authhero-widget.tsx). The page only renders a logo when the
|
|
238
|
+
caller opts into the chip variant. */
|
|
239
|
+
html[data-logo-position="widget"] .ah-chip-logo,
|
|
240
|
+
html[data-logo-position="none"] .ah-chip-logo { display: none; }
|
|
241
|
+
|
|
242
|
+
/* ============= FLOATING CHIPS =============
|
|
243
|
+
Self-contained pills positioned at page corners. Surface comes from
|
|
244
|
+
the chrome tokens above, so they adapt to mode + bg automatically. */
|
|
245
|
+
.ah-chip {
|
|
246
|
+
position: fixed;
|
|
247
|
+
z-index: 10;
|
|
248
|
+
background: var(--ah-chip-bg);
|
|
249
|
+
backdrop-filter: blur(20px);
|
|
250
|
+
-webkit-backdrop-filter: blur(20px);
|
|
251
|
+
border: 1px solid var(--ah-chip-border);
|
|
252
|
+
color: var(--ah-chip-fg);
|
|
253
|
+
border-radius: 9999px;
|
|
254
|
+
font-size: 12px;
|
|
255
|
+
font-weight: 500;
|
|
256
|
+
display: inline-flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
gap: 6px;
|
|
259
|
+
transition: background 200ms ease, color 200ms ease, border-color 200ms ease;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.ah-chip-logo {
|
|
263
|
+
top: 24px; left: 24px;
|
|
264
|
+
padding: 6px 14px 6px 8px;
|
|
265
|
+
background: var(--ah-chip-logo-bg);
|
|
266
|
+
}
|
|
267
|
+
.ah-chip-logo img { display: block; max-height: 20px; width: auto; }
|
|
268
|
+
.ah-chip-logo .ah-logo-text {
|
|
269
|
+
font-family: 'Inter Tight', 'Inter', system-ui, sans-serif;
|
|
270
|
+
font-weight: 700;
|
|
271
|
+
letter-spacing: 0.04em;
|
|
272
|
+
text-transform: uppercase;
|
|
273
|
+
font-size: 12px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.ah-chip-settings {
|
|
277
|
+
top: 24px; right: 24px;
|
|
278
|
+
padding: 4px;
|
|
279
|
+
gap: 0;
|
|
280
|
+
}
|
|
281
|
+
.ah-chip-settings button,
|
|
282
|
+
.ah-chip-settings .ah-lang {
|
|
283
|
+
background: 0; border: 0; padding: 6px 10px; cursor: pointer;
|
|
284
|
+
color: var(--ah-chip-fg-dim);
|
|
285
|
+
font-size: 12px; font-weight: 500;
|
|
286
|
+
border-radius: 9999px;
|
|
287
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
288
|
+
transition: 140ms;
|
|
289
|
+
}
|
|
290
|
+
.ah-chip-settings button:hover,
|
|
291
|
+
.ah-chip-settings .ah-lang:hover { color: var(--ah-chip-fg-strong); }
|
|
292
|
+
.ah-chip-settings .ah-lang select {
|
|
293
|
+
appearance: none; -webkit-appearance: none;
|
|
294
|
+
background: transparent; color: inherit; border: 0;
|
|
295
|
+
font: inherit; cursor: pointer; padding: 0;
|
|
296
|
+
outline: 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.ah-chip-trust {
|
|
300
|
+
bottom: 24px; left: 24px;
|
|
301
|
+
padding: 7px 14px 7px 10px;
|
|
302
|
+
color: var(--ah-chip-fg-mid);
|
|
303
|
+
}
|
|
304
|
+
.ah-chip-trust img { display: block; max-height: 18px; width: auto; opacity: 0.85; }
|
|
305
|
+
.ah-chip-trust:hover { color: var(--ah-chip-fg-strong); }
|
|
306
|
+
.ah-chip-trust a { color: inherit; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; }
|
|
307
|
+
|
|
308
|
+
/* Legal — chip when there's a bg image, plain text on solid bg */
|
|
309
|
+
.ah-chip-legal {
|
|
310
|
+
position: fixed;
|
|
311
|
+
bottom: 24px; right: 24px;
|
|
312
|
+
z-index: 10;
|
|
313
|
+
background: var(--ah-chip-bg);
|
|
314
|
+
backdrop-filter: blur(20px);
|
|
315
|
+
-webkit-backdrop-filter: blur(20px);
|
|
316
|
+
border: 1px solid var(--ah-chip-border);
|
|
317
|
+
border-radius: 9999px;
|
|
318
|
+
padding: 7px 14px;
|
|
319
|
+
color: var(--ah-legal-fg);
|
|
320
|
+
font-size: 11px;
|
|
321
|
+
letter-spacing: 0.04em;
|
|
322
|
+
display: inline-flex; align-items: center; gap: 10px;
|
|
323
|
+
transition: background 200ms ease, color 200ms ease, border-color 200ms ease;
|
|
324
|
+
}
|
|
325
|
+
html[data-bg="none"] .ah-chip-legal { padding: 4px 0; bottom: 28px; right: 28px; }
|
|
326
|
+
.ah-chip-legal a {
|
|
327
|
+
color: inherit; text-decoration: none;
|
|
328
|
+
transition: color 140ms;
|
|
329
|
+
}
|
|
330
|
+
.ah-chip-legal a:hover { color: var(--ah-legal-fg-hover); }
|
|
331
|
+
.ah-chip-legal .ah-sep { color: var(--ah-legal-sep); }
|
|
332
|
+
|
|
333
|
+
/* ============= PHONE FOOTER BAR =============
|
|
334
|
+
On a phone the corner chips have nowhere to float: the widget fills the
|
|
335
|
+
screen, so a fixed pill either covers the form or gets dropped (which
|
|
336
|
+
is what used to happen below 480px — terms and the trust mark vanished
|
|
337
|
+
and the settings chip sat on top of the card). The footer collects the
|
|
338
|
+
same four pieces into one strip along the bottom edge.
|
|
339
|
+
|
|
340
|
+
Always in the DOM, revealed only under the mobile breakpoint, so the
|
|
341
|
+
same markup serves both layouts with no user-agent sniffing. */
|
|
342
|
+
.ah-footer {
|
|
343
|
+
display: none;
|
|
344
|
+
position: fixed;
|
|
345
|
+
left: 0; right: 0; bottom: 0;
|
|
346
|
+
z-index: 20;
|
|
347
|
+
align-items: center;
|
|
348
|
+
justify-content: space-between;
|
|
349
|
+
gap: 8px;
|
|
350
|
+
padding: 6px 12px;
|
|
351
|
+
/* Clear the home indicator on notched phones. */
|
|
352
|
+
padding-bottom: max(6px, env(safe-area-inset-bottom, 6px));
|
|
353
|
+
background: var(--ah-footer-bg);
|
|
354
|
+
border-top: 1px solid var(--ah-footer-border);
|
|
355
|
+
backdrop-filter: blur(20px);
|
|
356
|
+
-webkit-backdrop-filter: blur(20px);
|
|
357
|
+
color: var(--ah-footer-fg);
|
|
358
|
+
font-size: 12px;
|
|
359
|
+
transition: background 200ms ease, color 200ms ease, border-color 200ms ease;
|
|
360
|
+
}
|
|
361
|
+
.ah-footer-left, .ah-footer-right {
|
|
362
|
+
display: inline-flex;
|
|
363
|
+
align-items: center;
|
|
364
|
+
gap: 8px;
|
|
365
|
+
min-width: 0;
|
|
366
|
+
}
|
|
367
|
+
/* Space is tight on a phone (a 375px bar has ~330px of usable width).
|
|
368
|
+
Priority order when it runs out: the controls and the terms link hold
|
|
369
|
+
their size, and the trust mark — decorative, and the only item that
|
|
370
|
+
scales without becoming unreadable — shrinks to fit. */
|
|
371
|
+
.ah-footer-left { flex: 1 1 auto; overflow: hidden; }
|
|
372
|
+
.ah-footer-right { flex: 0 0 auto; }
|
|
373
|
+
.ah-footer-trust { flex: 0 1 auto; min-width: 0; display: inline-flex; }
|
|
374
|
+
.ah-footer-trust a { display: inline-flex; align-items: center; min-width: 0; }
|
|
375
|
+
.ah-footer-trust img {
|
|
376
|
+
display: block;
|
|
377
|
+
max-width: 100%;
|
|
378
|
+
height: auto;
|
|
379
|
+
max-height: 14px;
|
|
380
|
+
opacity: 0.8;
|
|
381
|
+
}
|
|
382
|
+
/* Below this the bar can't hold a logo and a legal link at a legible
|
|
383
|
+
size, and the legal link is the one that has to stay. */
|
|
384
|
+
@media (max-width: 359px) {
|
|
385
|
+
.ah-footer-trust { display: none; }
|
|
386
|
+
}
|
|
387
|
+
.ah-footer-terms {
|
|
388
|
+
flex: 0 0 auto;
|
|
389
|
+
color: inherit;
|
|
390
|
+
text-decoration: none;
|
|
391
|
+
white-space: nowrap;
|
|
392
|
+
transition: color 140ms;
|
|
393
|
+
}
|
|
394
|
+
.ah-footer-terms:hover { color: var(--ah-footer-fg-strong); }
|
|
395
|
+
.ah-footer button,
|
|
396
|
+
.ah-footer .ah-lang {
|
|
397
|
+
background: 0; border: 0; padding: 7px 8px; cursor: pointer;
|
|
398
|
+
color: inherit;
|
|
399
|
+
font-size: 12px; font-weight: 500;
|
|
400
|
+
min-height: 34px;
|
|
401
|
+
border-radius: 10px;
|
|
402
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
403
|
+
transition: 140ms;
|
|
404
|
+
}
|
|
405
|
+
.ah-footer button:hover,
|
|
406
|
+
.ah-footer .ah-lang:hover,
|
|
407
|
+
.ah-footer button:focus-visible,
|
|
408
|
+
.ah-footer .ah-lang:focus-within {
|
|
409
|
+
color: var(--ah-footer-fg-strong);
|
|
410
|
+
background: var(--ah-chip-active-bg);
|
|
411
|
+
}
|
|
412
|
+
/* The globe is redundant beside a language name and costs ~18px of a
|
|
413
|
+
budget that doesn't have it — the chip keeps it, the footer doesn't. */
|
|
414
|
+
.ah-footer .ah-lang > svg { display: none; }
|
|
415
|
+
.ah-footer .ah-lang select {
|
|
416
|
+
appearance: none; -webkit-appearance: none;
|
|
417
|
+
background: transparent; color: inherit; border: 0;
|
|
418
|
+
font: inherit; cursor: pointer; padding: 0;
|
|
419
|
+
outline: 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* ============= PER-SLOT CHIP STYLE OVERRIDES =============
|
|
423
|
+
Templates can force a chip's surface via the slot tag's style arg
|
|
424
|
+
(e.g. {%- authhero:legal style="plain" -%}). Without it chips follow
|
|
425
|
+
the data-bg default (pill with a background image, plain on a solid
|
|
426
|
+
background). These modifiers re-point the surface tokens directly on
|
|
427
|
+
the element, so they win over the inherited data-bg values. */
|
|
428
|
+
.ah-chip--pill {
|
|
429
|
+
--ah-chip-bg: var(--ah-chip-bg-pill);
|
|
430
|
+
--ah-chip-bg-hover: var(--ah-chip-bg-hover-pill);
|
|
431
|
+
--ah-chip-border: var(--ah-chip-border-pill);
|
|
432
|
+
--ah-chip-logo-bg: var(--ah-chip-logo-bg-pill);
|
|
433
|
+
backdrop-filter: blur(20px);
|
|
434
|
+
-webkit-backdrop-filter: blur(20px);
|
|
435
|
+
}
|
|
436
|
+
.ah-chip-legal.ah-chip--pill { padding: 7px 14px; bottom: 24px; right: 24px; }
|
|
437
|
+
|
|
438
|
+
.ah-chip--plain {
|
|
439
|
+
--ah-chip-bg: transparent;
|
|
440
|
+
--ah-chip-bg-hover: transparent;
|
|
441
|
+
--ah-chip-border: transparent;
|
|
442
|
+
--ah-chip-logo-bg: transparent;
|
|
443
|
+
backdrop-filter: none;
|
|
444
|
+
-webkit-backdrop-filter: none;
|
|
445
|
+
}
|
|
446
|
+
.ah-chip-legal.ah-chip--plain { padding: 4px 0; bottom: 28px; right: 28px; }
|
|
447
|
+
|
|
448
|
+
/* ============= EXPLICIT DARK MODE FOR WIDGET =============
|
|
449
|
+
The page-level dark/light is controlled by data-mode (above).
|
|
450
|
+
The widget itself has its own --ah-color-* vars set via JS.
|
|
451
|
+
html.ah-dark-mode is the legacy class kept for the widget toggle. */
|
|
452
|
+
${darkModeCssVarRules("html.ah-dark-mode authhero-widget", primaryColor || themePrimary)}
|
|
453
|
+
@media (prefers-color-scheme: dark) {
|
|
454
|
+
${darkModeCssVarRules("html:not(.ah-light-mode) authhero-widget", primaryColor || themePrimary)}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* ============= MOBILE =============
|
|
458
|
+
Chrome chips minimize, and the card either floats on the page
|
|
459
|
+
background image or fills the viewport when there isn't one — the
|
|
460
|
+
two variants are emitted conditionally at the end of this block.
|
|
461
|
+
|
|
462
|
+
The !important flags are load-bearing: the body-fragment path sets
|
|
463
|
+
the body layout inline (see buildBodyLayout) and the widget
|
|
464
|
+
container's width inline (see buildWidgetContainerStyle), and an
|
|
465
|
+
inline declaration outranks a normal rule from this stylesheet.
|
|
466
|
+
|
|
467
|
+
Below this width a page_layout of "left"/"right" collapses to
|
|
468
|
+
centered: the 80px offset only reads as a deliberate composition when
|
|
469
|
+
there's page background left over beside the widget. On a phone or a
|
|
470
|
+
narrow window the offset just pushes a 400px card off-centre (and, at
|
|
471
|
+
the low end, off-screen). */
|
|
472
|
+
@media (max-width: 767px) {
|
|
473
|
+
body { justify-content: center !important; padding: 20px !important; }
|
|
474
|
+
}
|
|
475
|
+
@media (max-width: 480px) {
|
|
476
|
+
/* Full width in both variants: overrides the inline
|
|
477
|
+
clamp(320px, 100%, 400px), which would otherwise hold the card at
|
|
478
|
+
400px with stray gutters on a 400-480px phone. */
|
|
479
|
+
.widget-container { width: 100% !important; }
|
|
480
|
+
.ah-widget-stack { width: 100% !important; }
|
|
481
|
+
/* Every corner chip gives way to the footer bar, which carries the
|
|
482
|
+
same content in a strip that can't overlap the form. */
|
|
483
|
+
.ah-chip, .ah-chip-legal { display: none; }
|
|
484
|
+
.ah-footer { display: flex; }
|
|
485
|
+
}
|
|
486
|
+
${hasBgImage
|
|
487
|
+
? `
|
|
488
|
+
/* Phone, WITH a page background image — the card floats.
|
|
489
|
+
|
|
490
|
+
The image is the tenant's branding; going full-bleed here would hide
|
|
491
|
+
it entirely on the device most sign-ins come from. So the body keeps
|
|
492
|
+
its background (no override), the tint overlay stays for chip
|
|
493
|
+
legibility, and the card keeps a gutter so the image reads around it.
|
|
494
|
+
The widget's own stylesheet is told to match via the \`floating\`
|
|
495
|
+
attribute (see renderWidgetSSR). */
|
|
496
|
+
@media (max-width: 480px) {
|
|
497
|
+
/* Extra bottom padding keeps the floating card clear of the footer. */
|
|
498
|
+
body { padding: 20px 20px 76px !important; }
|
|
499
|
+
.ah-bg-tint { display: block; }
|
|
500
|
+
}`
|
|
501
|
+
: `
|
|
502
|
+
/* Phone, NO page background image — the card fills the screen.
|
|
503
|
+
|
|
504
|
+
With only a flat colour behind it there is nothing to show around the
|
|
505
|
+
card, so it goes edge-to-edge and the page background collapses into
|
|
506
|
+
the widget's own colour. Matches the widget stylesheet's default
|
|
507
|
+
(non-\`floating\`) mobile layout. */
|
|
508
|
+
@media (max-width: 480px) {
|
|
509
|
+
body { background: ${widgetBackground} !important; padding: 0 !important; }
|
|
510
|
+
html.ah-dark-mode body { background: #111827 !important; }
|
|
511
|
+
.ah-bg-tint { display: none; }
|
|
512
|
+
/* The full-bleed widget is exactly 100vh, so pad its bottom (a
|
|
513
|
+
light-DOM rule on the host beats the :host rule in its shadow tree)
|
|
514
|
+
to stop a tall form scrolling under the fixed footer. The footer's
|
|
515
|
+
own bottom padding grows with the safe-area inset, so the clearance
|
|
516
|
+
has to track it or the footer covers the last field on a notched
|
|
517
|
+
phone — mirror that \`max()\` rather than hard-coding 60px. */
|
|
518
|
+
authhero-widget {
|
|
519
|
+
padding-bottom: calc(54px + max(6px, env(safe-area-inset-bottom, 6px)));
|
|
520
|
+
}
|
|
521
|
+
}`}
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML/CSS escaping and sanitization for the page chrome.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained on purpose: this module is consumed both by the authhero
|
|
5
|
+
* server (which has its own copy of these primitives in
|
|
6
|
+
* `sanitization-utils.ts` for its other routes) and by the widget demo
|
|
7
|
+
* server. Security primitives are deliberately NOT shared across the package
|
|
8
|
+
* boundary — they are small, stable, and a broken import here would silently
|
|
9
|
+
* weaken escaping on every login page.
|
|
10
|
+
*/
|
|
11
|
+
/** Escape HTML special characters to prevent XSS. */
|
|
12
|
+
export function escapeHtml(str) {
|
|
13
|
+
return str
|
|
14
|
+
.replace(/&/g, "&")
|
|
15
|
+
.replace(/</g, "<")
|
|
16
|
+
.replace(/>/g, ">")
|
|
17
|
+
.replace(/"/g, """)
|
|
18
|
+
.replace(/'/g, "'");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Escape a URL for safe use inside a CSS `url("...")` function, so a crafted
|
|
22
|
+
* image URL can't break out of the url() context and inject rules.
|
|
23
|
+
*/
|
|
24
|
+
export function escapeCssUrl(url) {
|
|
25
|
+
return url
|
|
26
|
+
.replace(/\\/g, "\\\\")
|
|
27
|
+
.replace(/"/g, '\\"')
|
|
28
|
+
.replace(/'/g, "\\'")
|
|
29
|
+
.replace(/\(/g, "\\(")
|
|
30
|
+
.replace(/\)/g, "\\)")
|
|
31
|
+
.replace(/\n/g, "")
|
|
32
|
+
.replace(/\r/g, "")
|
|
33
|
+
.replace(/\t/g, "");
|
|
34
|
+
}
|
|
35
|
+
/** True for URLs we're willing to emit: http(s)/data, or root-relative. */
|
|
36
|
+
function isAllowedUrl(url) {
|
|
37
|
+
try {
|
|
38
|
+
return ["http:", "https:", "data:"].includes(new URL(url).protocol);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return url.startsWith("/");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Sanitize a URL for use in href/src attributes. Returns "" if unsafe. */
|
|
45
|
+
export function sanitizeUrl(url) {
|
|
46
|
+
if (!url)
|
|
47
|
+
return "";
|
|
48
|
+
return isAllowedUrl(url) ? escapeHtml(url) : "";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Sanitize a URL for use inside a CSS `url("...")`. Returns "" if unsafe.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately not `sanitizeUrl` + `escapeCssUrl`: HTML entities are never
|
|
54
|
+
* decoded inside a `<style>` element, so HTML-escaping here would put the
|
|
55
|
+
* literal text `&` into the stylesheet and corrupt every query string it
|
|
56
|
+
* touches — a signed `?sig=x&expires=y` would be requested as
|
|
57
|
+
* `?sig=x&expires=y` and the image would fail to load.
|
|
58
|
+
*
|
|
59
|
+
* Angle brackets still have to go, or a `</style>` inside the URL would end
|
|
60
|
+
* the element and let the rest of it parse as markup. They are percent-encoded
|
|
61
|
+
* rather than entity-encoded so the URL stays a valid URL.
|
|
62
|
+
*/
|
|
63
|
+
export function sanitizeCssUrl(url) {
|
|
64
|
+
if (!url)
|
|
65
|
+
return "";
|
|
66
|
+
if (!isAllowedUrl(url))
|
|
67
|
+
return "";
|
|
68
|
+
return escapeCssUrl(url.replace(/</g, "%3C").replace(/>/g, "%3E"));
|
|
69
|
+
}
|
|
70
|
+
/** Sanitize a CSS color value. Returns "" if it doesn't look like a color. */
|
|
71
|
+
export function sanitizeCssColor(color) {
|
|
72
|
+
if (!color)
|
|
73
|
+
return "";
|
|
74
|
+
const safeColorPattern = /^(#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|[a-zA-Z]+)$/;
|
|
75
|
+
if (safeColorPattern.test(color.trim())) {
|
|
76
|
+
return color.trim();
|
|
77
|
+
}
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
/** Build a CSS `background` value from a branding page_background. */
|
|
81
|
+
export function buildPageBackground(pageBackground) {
|
|
82
|
+
if (!pageBackground)
|
|
83
|
+
return "#f5f5f5";
|
|
84
|
+
if (typeof pageBackground === "string") {
|
|
85
|
+
return sanitizeCssColor(pageBackground) || "#f5f5f5";
|
|
86
|
+
}
|
|
87
|
+
const { type, start, end, angle_deg } = pageBackground;
|
|
88
|
+
if (type === "linear-gradient" && start && end) {
|
|
89
|
+
const sanitizedStart = sanitizeCssColor(start);
|
|
90
|
+
const sanitizedEnd = sanitizeCssColor(end);
|
|
91
|
+
if (sanitizedStart && sanitizedEnd) {
|
|
92
|
+
const angle = typeof angle_deg === "number" ? angle_deg : 180;
|
|
93
|
+
return `linear-gradient(${angle}deg, ${sanitizedStart}, ${sanitizedEnd})`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (start) {
|
|
97
|
+
const sanitizedColor = sanitizeCssColor(start);
|
|
98
|
+
if (sanitizedColor)
|
|
99
|
+
return sanitizedColor;
|
|
100
|
+
}
|
|
101
|
+
return "#f5f5f5";
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build a CSS `background` value from a theme page_background, falling back
|
|
105
|
+
* to the branding one. Supports `background_image_url`.
|
|
106
|
+
*/
|
|
107
|
+
export function buildThemePageBackground(themePageBackground, fallbackBrandingBackground) {
|
|
108
|
+
if (themePageBackground?.background_image_url) {
|
|
109
|
+
const imageUrl = sanitizeCssUrl(themePageBackground.background_image_url);
|
|
110
|
+
if (imageUrl) {
|
|
111
|
+
const bgColor = sanitizeCssColor(themePageBackground.background_color) || "#f5f5f5";
|
|
112
|
+
return `${bgColor} url("${imageUrl}") center / cover no-repeat`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (themePageBackground?.background_color) {
|
|
116
|
+
const bgColor = sanitizeCssColor(themePageBackground.background_color);
|
|
117
|
+
if (bgColor)
|
|
118
|
+
return bgColor;
|
|
119
|
+
}
|
|
120
|
+
return buildPageBackground(fallbackBrandingBackground);
|
|
121
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login page chrome — the furniture around the widget card.
|
|
3
|
+
*
|
|
4
|
+
* Shared between the authhero login page (which renders it for real) and the
|
|
5
|
+
* widget demo server (which previews it), so the two can't drift. Everything
|
|
6
|
+
* here is framework-free: plain strings in, plain strings out, no JSX runtime
|
|
7
|
+
* and no i18n stack.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./types";
|
|
10
|
+
export * from "./html";
|
|
11
|
+
export * from "./colors";
|
|
12
|
+
export * from "./chrome";
|
|
13
|
+
export * from "./css";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|