central-auth-js-sdk 1.0.10

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/theme.js ADDED
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Central Auth SDK — Theme Engine
3
+ *
4
+ * Applies optional custom theme to the SDK container via CSS custom properties.
5
+ * All theme properties are optional — the SDK's built-in warm theme applies
6
+ * for anything the host app doesn't override.
7
+ *
8
+ * @module theme
9
+ */
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Color utilities
13
+ // ---------------------------------------------------------------------------
14
+
15
+ function hexToRgb(hex) {
16
+ hex = hex.replace('#', '');
17
+ if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
18
+ const n = parseInt(hex, 16);
19
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
20
+ }
21
+
22
+ function rgbToHex(r, g, b) {
23
+ return '#' + [r, g, b]
24
+ .map(v => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0'))
25
+ .join('');
26
+ }
27
+
28
+ function relativeLuminance(hex) {
29
+ const [r, g, b] = hexToRgb(hex).map(v => {
30
+ v /= 255;
31
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
32
+ });
33
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
34
+ }
35
+
36
+ function isDark(hex) {
37
+ return relativeLuminance(hex) < 0.35;
38
+ }
39
+
40
+ function lighten(hex, amount) {
41
+ const [r, g, b] = hexToRgb(hex);
42
+ return rgbToHex(
43
+ r + (255 - r) * amount,
44
+ g + (255 - g) * amount,
45
+ b + (255 - b) * amount,
46
+ );
47
+ }
48
+
49
+ function darken(hex, amount) {
50
+ const [r, g, b] = hexToRgb(hex);
51
+ return rgbToHex(r * (1 - amount), g * (1 - amount), b * (1 - amount));
52
+ }
53
+
54
+ function withAlpha(hex, alpha) {
55
+ const [r, g, b] = hexToRgb(hex);
56
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
57
+ }
58
+
59
+ function contrastRatio(hex1, hex2) {
60
+ const l1 = relativeLuminance(hex1);
61
+ const l2 = relativeLuminance(hex2);
62
+ const lighter = Math.max(l1, l2);
63
+ const darker = Math.min(l1, l2);
64
+ return (lighter + 0.05) / (darker + 0.05);
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Theme application
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /**
72
+ * Apply a theme to the SDK layout root element.
73
+ *
74
+ * @param {HTMLElement} layoutRoot - The `.wf-layout` element created by createLayout
75
+ * @param {Object} theme
76
+ * @param {string} [theme.primaryColor] - Primary brand color (hex). Buttons, links, active states.
77
+ * @param {string} [theme.secondaryColor] - Secondary accent color (hex). Reserved for future use.
78
+ * @param {string} [theme.backgroundColor] - Page / layout background color (hex).
79
+ * @param {string} [theme.backgroundImage] - URL for a full-bleed background image.
80
+ * @param {string} [theme.cardColor] - Card panel background color (hex).
81
+ * @param {string} [theme.textColor] - Primary text color (hex).
82
+ * @param {string} [theme.fontFamily] - CSS font-family string.
83
+ * @param {'light'|'dark'|'auto'} [theme.mode] - Color mode. 'auto' (default) detects from backgroundColor.
84
+ * @returns {Function} cleanup — call to remove all applied theme styles
85
+ */
86
+ export function applyTheme(layoutRoot, theme) {
87
+ if (!theme || typeof theme !== 'object' || !layoutRoot) return noop;
88
+
89
+ const hasAny = theme.primaryColor || theme.secondaryColor || theme.backgroundColor
90
+ || theme.backgroundImage || theme.cardColor || theme.textColor
91
+ || theme.fontFamily || theme.mode;
92
+ if (!hasAny) return noop;
93
+
94
+ const bgEl = layoutRoot.querySelector('.wf-bg');
95
+ const appliedVars = [];
96
+ const inlineBackups = new Map();
97
+
98
+ function setVar(name, value) {
99
+ if (value == null) return;
100
+ layoutRoot.style.setProperty(name, value);
101
+ appliedVars.push(name);
102
+ }
103
+
104
+ function backupInline(el) {
105
+ if (!el || inlineBackups.has(el)) return;
106
+ inlineBackups.set(el, el.getAttribute('style') || '');
107
+ }
108
+
109
+ // -- Detect color mode --
110
+ const explicitMode = (theme.mode === 'dark' || theme.mode === 'light') ? theme.mode : null;
111
+ const bgColor = theme.backgroundColor;
112
+ const mode = explicitMode
113
+ || (bgColor ? (isDark(bgColor) ? 'dark' : 'light') : 'light');
114
+
115
+ // -- Primary color derivations --
116
+ const primary = theme.primaryColor;
117
+ if (primary) {
118
+ setVar('--wf-primary', primary);
119
+ setVar('--wf-primary-dark', darken(primary, 0.15));
120
+ setVar('--wf-primary-light', withAlpha(primary, 0.07));
121
+ setVar('--wf-primary-glow', withAlpha(primary, 0.18));
122
+ setVar('--wf-primary-grad-start', lighten(primary, 0.2));
123
+ setVar('--wf-primary-grad-end', darken(primary, 0.12));
124
+ setVar('--wf-face-border', withAlpha(primary, 0.5));
125
+
126
+ setVar('--wf-shadow-btn',
127
+ `0 1px 3px rgba(0,0,0,0.06), 0 4px 12px ${withAlpha(primary, 0.18)}`);
128
+ setVar('--wf-shadow-btn-hover',
129
+ `0 2px 6px rgba(0,0,0,0.08), 0 8px 24px ${withAlpha(primary, 0.22)}`);
130
+
131
+ setVar('--wf-orb-1', withAlpha(lighten(primary, 0.45), 0.4));
132
+ setVar('--wf-orb-2', withAlpha(lighten(primary, 0.50), 0.35));
133
+ setVar('--wf-orb-3', withAlpha(lighten(primary, 0.55), 0.3));
134
+
135
+ // WCAG AA check (3:1 minimum for UI components)
136
+ const cardBg = theme.cardColor || (mode === 'dark' ? '#1E1E2E' : '#FFFFFF');
137
+ const ratio = contrastRatio(primary, cardBg);
138
+ if (ratio < 3) {
139
+ console.warn(
140
+ `[Central Auth SDK] Theme: primaryColor "${primary}" has low contrast (${ratio.toFixed(1)}) against card background "${cardBg}". ` +
141
+ 'WCAG AA requires ≥ 3:1 for UI components. Consider a bolder color.',
142
+ );
143
+ }
144
+ }
145
+
146
+ // -- Background color --
147
+ if (bgColor) {
148
+ setVar('--wf-bg', bgColor);
149
+ }
150
+
151
+ // -- Card color --
152
+ const cardColor = theme.cardColor || (mode === 'dark' ? '#1E1E2E' : null);
153
+ if (cardColor) {
154
+ setVar('--wf-card', cardColor);
155
+ setVar('--wf-input-bg',
156
+ mode === 'dark' ? lighten(cardColor, 0.05) : darken(cardColor, 0.015));
157
+ }
158
+
159
+ // -- Text color --
160
+ if (theme.textColor) {
161
+ setVar('--wf-fg', theme.textColor);
162
+ const [r, g, b] = hexToRgb(theme.textColor);
163
+ setVar('--wf-fg-soft', `rgba(${r},${g},${b},0.75)`);
164
+ } else if (mode === 'dark') {
165
+ setVar('--wf-fg', '#E8E6E3');
166
+ setVar('--wf-fg-soft', '#B8B4AE');
167
+ }
168
+
169
+ // -- Dark mode surface/border adjustments --
170
+ if (mode === 'dark') {
171
+ if (!theme.textColor) {
172
+ setVar('--wf-muted', '#8A857E');
173
+ setVar('--wf-muted-bg', 'rgba(255,255,255,0.06)');
174
+ }
175
+ setVar('--wf-border', 'rgba(255,255,255,0.1)');
176
+ setVar('--wf-border-hover', 'rgba(255,255,255,0.18)');
177
+ setVar('--wf-shadow-card',
178
+ '0 0 0 1px rgba(255,255,255,0.05), 0 4px 24px rgba(0,0,0,0.3)');
179
+ setVar('--wf-shadow-input', '0 1px 2px rgba(0,0,0,0.15)');
180
+ }
181
+
182
+ // -- Secondary color (stored for future use) --
183
+ if (theme.secondaryColor) {
184
+ setVar('--wf-secondary', theme.secondaryColor);
185
+ }
186
+
187
+ // -- Font --
188
+ if (theme.fontFamily) {
189
+ setVar('--wf-font', theme.fontFamily);
190
+ }
191
+
192
+ // -- Background image / mesh gradient override --
193
+ if (bgEl) {
194
+ if (theme.backgroundImage) {
195
+ backupInline(bgEl);
196
+ layoutRoot.setAttribute('data-wf-bg-image', '');
197
+ const overlay = mode === 'dark'
198
+ ? 'rgba(0,0,0,0.55)'
199
+ : 'rgba(255,255,255,0.6)';
200
+ bgEl.style.background =
201
+ `linear-gradient(${overlay}, ${overlay}), url("${theme.backgroundImage}") center/cover no-repeat`;
202
+ bgEl.querySelectorAll('.wf-bg-orb').forEach(orb => { orb.style.display = 'none'; });
203
+ } else if (primary || bgColor) {
204
+ backupInline(bgEl);
205
+ const p = primary || '#D46B2D';
206
+ const bg = bgColor || '#F5F1EC';
207
+ bgEl.style.background = [
208
+ `radial-gradient(ellipse 80% 50% at 20% 80%, ${withAlpha(lighten(p, 0.45), 0.7)} 0%, transparent 55%)`,
209
+ `radial-gradient(ellipse 60% 60% at 85% 15%, ${withAlpha(lighten(p, 0.50), 0.5)} 0%, transparent 50%)`,
210
+ `radial-gradient(ellipse 50% 50% at 50% 50%, ${withAlpha(lighten(p, 0.55), 0.4)} 0%, transparent 60%)`,
211
+ bg,
212
+ ].join(', ');
213
+ }
214
+ }
215
+
216
+ // Store mode for any CSS hooks ([data-wf-mode="dark"])
217
+ layoutRoot.setAttribute('data-wf-mode', mode);
218
+
219
+ // -- Cleanup --
220
+ return function cleanup() {
221
+ appliedVars.forEach(v => layoutRoot.style.removeProperty(v));
222
+ layoutRoot.removeAttribute('data-wf-mode');
223
+ layoutRoot.removeAttribute('data-wf-bg-image');
224
+ inlineBackups.forEach((backup, el) => {
225
+ if (backup) el.setAttribute('style', backup);
226
+ else el.removeAttribute('style');
227
+ });
228
+ if (bgEl) {
229
+ bgEl.querySelectorAll('.wf-bg-orb').forEach(orb => { orb.style.display = ''; });
230
+ }
231
+ };
232
+ }
233
+
234
+ function noop() {}
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Built-in presets
238
+ // ---------------------------------------------------------------------------
239
+
240
+ /** @type {Record<string, Object|null>} */
241
+ export const THEME_PRESETS = {
242
+ default: null,
243
+ dark: {
244
+ primaryColor: '#E8985A',
245
+ backgroundColor: '#0F0F14',
246
+ cardColor: '#1A1A24',
247
+ textColor: '#E8E6E3',
248
+ mode: 'dark',
249
+ },
250
+ ocean: {
251
+ primaryColor: '#2563EB',
252
+ backgroundColor: '#EFF6FF',
253
+ secondaryColor: '#1E40AF',
254
+ },
255
+ forest: {
256
+ primaryColor: '#16A34A',
257
+ backgroundColor: '#F0FDF4',
258
+ secondaryColor: '#166534',
259
+ },
260
+ purple: {
261
+ primaryColor: '#7C3AED',
262
+ backgroundColor: '#FAF5FF',
263
+ secondaryColor: '#5B21B6',
264
+ },
265
+ rose: {
266
+ primaryColor: '#E11D48',
267
+ backgroundColor: '#FFF1F2',
268
+ secondaryColor: '#9F1239',
269
+ },
270
+ midnight: {
271
+ primaryColor: '#818CF8',
272
+ backgroundColor: '#0F172A',
273
+ cardColor: '#1E293B',
274
+ textColor: '#E2E8F0',
275
+ mode: 'dark',
276
+ },
277
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Reusable UI primitives - Vanilla JS components
3
+ */
4
+
5
+ export function createElement(tag, attrs = {}, children = []) {
6
+ const el = document.createElement(tag);
7
+ Object.entries(attrs).forEach(([key, value]) => {
8
+ if (value == null) return;
9
+ if (key === 'className') {
10
+ el.className = value;
11
+ } else if (key === 'style' && typeof value === 'object') {
12
+ Object.assign(el.style, value);
13
+ } else if (key.startsWith('on') && typeof value === 'function') {
14
+ el.addEventListener(key.slice(2).toLowerCase(), value);
15
+ } else if (key === 'html') {
16
+ el.innerHTML = value;
17
+ } else if (key === 'ref' && typeof value === 'function') {
18
+ value(el);
19
+ } else if (key === 'dataset' && typeof value === 'object') {
20
+ Object.assign(el.dataset, value);
21
+ } else if (typeof value === 'boolean') {
22
+ if (value) el.setAttribute(key, '');
23
+ } else if (key !== 'ref') {
24
+ el.setAttribute(key, value);
25
+ }
26
+ });
27
+ children.forEach((child) => {
28
+ if (typeof child === 'string') {
29
+ el.appendChild(document.createTextNode(child));
30
+ } else if (child && child.nodeType) {
31
+ el.appendChild(child);
32
+ } else if (Array.isArray(child)) {
33
+ child.forEach((c) => el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c));
34
+ }
35
+ });
36
+ return el;
37
+ }
38
+
39
+ export function button(text, { variant = 'default', size = 'lg', onClick, className = '', icon, disabled } = {}) {
40
+ const btn = createElement('button', {
41
+ type: 'button',
42
+ className: `wf-btn wf-btn-${variant} wf-btn-${size} ${className}`.trim(),
43
+ onClick,
44
+ disabled,
45
+ });
46
+ if (icon) btn.appendChild(icon);
47
+ btn.appendChild(document.createTextNode(text));
48
+ return btn;
49
+ }
50
+
51
+ export function input({ id, type = 'text', placeholder, value = '', className = '', required, maxLength, inputMode } = {}) {
52
+ return createElement('input', {
53
+ id,
54
+ type,
55
+ placeholder,
56
+ value,
57
+ className: `wf-input ${className}`.trim(),
58
+ required,
59
+ maxLength,
60
+ inputMode,
61
+ });
62
+ }
63
+
64
+ export function label(text, { htmlFor, className = '' } = {}) {
65
+ return createElement('label', {
66
+ htmlFor,
67
+ className: `wf-label ${className}`.trim(),
68
+ }, [text]);
69
+ }
70
+
71
+ export function iconSvg(paths) {
72
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
73
+ svg.setAttribute('viewBox', '0 0 24 24');
74
+ svg.setAttribute('width', '24');
75
+ svg.setAttribute('height', '24');
76
+ svg.setAttribute('fill', 'none');
77
+ svg.setAttribute('stroke', 'currentColor');
78
+ svg.setAttribute('stroke-width', '2');
79
+ svg.setAttribute('stroke-linecap', 'round');
80
+ svg.setAttribute('stroke-linejoin', 'round');
81
+ svg.classList.add('wf-icon');
82
+ if (typeof paths === 'string') {
83
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
84
+ path.setAttribute('d', paths);
85
+ svg.appendChild(path);
86
+ } else {
87
+ paths.forEach((d) => {
88
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
89
+ path.setAttribute('d', d);
90
+ svg.appendChild(path);
91
+ });
92
+ }
93
+ return svg;
94
+ }
package/ui/layout.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Layout v3 — Centered card on animated gradient mesh
3
+ * Features an SVG progress ring instead of a traditional step bar
4
+ */
5
+
6
+ const SHIELD_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`;
7
+
8
+ const CIRCUMFERENCE = 100;
9
+ const RING_R = 15.9155;
10
+
11
+ export function createLayout(container) {
12
+ container.innerHTML = '';
13
+
14
+ const root = document.createElement('div');
15
+ root.className = 'wf-layout';
16
+ root.innerHTML = `
17
+ <div class="wf-bg" aria-hidden="true">
18
+ <div class="wf-bg-orb wf-bg-orb-1"></div>
19
+ <div class="wf-bg-orb wf-bg-orb-2"></div>
20
+ <div class="wf-bg-orb wf-bg-orb-3"></div>
21
+ </div>
22
+ <div class="wf-card">
23
+ <div class="wf-card-accent"></div>
24
+ <div class="wf-card-inner">
25
+ <div class="wf-card-header">
26
+ <div class="wf-card-brand">
27
+ <div class="wf-card-logo">${SHIELD_SVG}</div>
28
+ <span class="wf-card-brand-name">Central Auth</span>
29
+ </div>
30
+ <div class="wf-ring" role="progressbar" aria-valuenow="1" aria-valuemin="1" aria-valuemax="7" aria-label="Step 1 of 7">
31
+ <svg class="wf-ring-svg" viewBox="0 0 36 36">
32
+ <defs>
33
+ <linearGradient id="wf-ring-grad" x1="0%" y1="0%" x2="100%" y2="100%">
34
+ <stop class="wf-ring-grad-start" offset="0%" stop-color="#E8985A"/>
35
+ <stop class="wf-ring-grad-end" offset="100%" stop-color="#C45828"/>
36
+ </linearGradient>
37
+ </defs>
38
+ <circle class="wf-ring-track" cx="18" cy="18" r="${RING_R}"/>
39
+ <circle class="wf-ring-fill" cx="18" cy="18" r="${RING_R}"
40
+ stroke="url(#wf-ring-grad)"
41
+ stroke-dasharray="${CIRCUMFERENCE}"
42
+ stroke-dashoffset="${CIRCUMFERENCE}"/>
43
+ </svg>
44
+ <span class="wf-ring-num" id="wf-ring-num"></span>
45
+ </div>
46
+ </div>
47
+ <div class="wf-card-body">
48
+ <div class="wf-error-banner hidden" id="wf-error-banner" role="alert"></div>
49
+ <div id="wf-form-inner"></div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ <p class="wf-page-footer">&copy; ${new Date().getFullYear()} Central Auth. All rights reserved.</p>
54
+ `;
55
+
56
+ container.appendChild(root);
57
+
58
+ const formInner = root.querySelector('#wf-form-inner');
59
+ const ringFill = root.querySelector('.wf-ring-fill');
60
+ const ringNum = root.querySelector('#wf-ring-num');
61
+ const ring = root.querySelector('.wf-ring');
62
+ const errorBanner = root.querySelector('#wf-error-banner');
63
+
64
+ function setContent(el) {
65
+ formInner.innerHTML = '';
66
+ if (el) formInner.appendChild(el);
67
+ }
68
+
69
+ function updateProgress(current, total) {
70
+ const pct = total > 0 ? (current / total) * CIRCUMFERENCE : 0;
71
+ ringFill.style.strokeDashoffset = CIRCUMFERENCE - pct;
72
+
73
+ ring.setAttribute('aria-valuenow', current);
74
+ ring.setAttribute('aria-valuemax', total);
75
+ ring.setAttribute('aria-label', `Step ${current} of ${total}`);
76
+
77
+ ringNum.textContent = current;
78
+ ringNum.classList.remove('wf-ring-pop');
79
+ void ringNum.offsetWidth;
80
+ ringNum.classList.add('wf-ring-pop');
81
+ }
82
+
83
+ function showError(message) {
84
+ if (!message) {
85
+ errorBanner.classList.add('hidden');
86
+ errorBanner.textContent = '';
87
+ return;
88
+ }
89
+ errorBanner.textContent = message;
90
+ errorBanner.classList.remove('hidden');
91
+ }
92
+
93
+ return {
94
+ root,
95
+ setContent,
96
+ updateProgress,
97
+ showError,
98
+ renderContent: setContent,
99
+ };
100
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Screen 1 — Create your account
3
+ * Collects first name, last name, and email address
4
+ * Social sign-up buttons (Google / Microsoft) shown as alternative
5
+ */
6
+
7
+ const GOOGLE_SVG = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.27-4.74 3.27-8.1z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09a6.95 6.95 0 0 1 0-4.17V7.07H2.18A11.97 11.97 0 0 0 0 12c0 1.94.46 3.77 1.28 5.4l3.56-2.77-.01-.54z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>`;
8
+
9
+ const MS_SVG = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="10" height="10" fill="#F25022"/><rect x="13" y="1" width="10" height="10" fill="#7FBA00"/><rect x="1" y="13" width="10" height="10" fill="#00A4EF"/><rect x="13" y="13" width="10" height="10" fill="#FFB900"/></svg>`;
10
+
11
+ const USER_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`;
12
+
13
+ const MAIL_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 7l-10 7L2 7"/></svg>`;
14
+
15
+ const ARROW_LEFT = '<path d="M19 12H5m7 7l-7-7 7-7"/>';
16
+
17
+ export function createEmailEntryScreen(onNext, onSocialSignUp, onSwitchToLogin, onBackToApp) {
18
+ const screen = document.createElement('div');
19
+ screen.className = 'wf-screen';
20
+
21
+ screen.innerHTML = `
22
+ <div class="wf-screen-back"><button type="button" class="wf-btn wf-btn-ghost wf-btn-lg" id="wf-email-back" aria-label="Back"><svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${ARROW_LEFT}</svg> Back</button></div>
23
+ <div class="wf-screen-header">
24
+ <div class="wf-icon-box">${USER_ICON}</div>
25
+ <h2 class="wf-screen-title">Create your account</h2>
26
+ <p class="wf-screen-desc">Get started — it only takes a minute</p>
27
+ </div>
28
+
29
+ <div class="wf-social-buttons">
30
+ <button type="button" class="wf-btn wf-btn-social wf-btn-flex wf-btn-lg" id="wf-social-google">
31
+ <span class="wf-icon-sm">${GOOGLE_SVG}</span>
32
+ Google
33
+ </button>
34
+ <button type="button" class="wf-btn wf-btn-social wf-btn-flex wf-btn-lg" id="wf-social-ms">
35
+ <span class="wf-icon-sm">${MS_SVG}</span>
36
+ Microsoft
37
+ </button>
38
+ </div>
39
+
40
+ <div class="wf-divider"><span>or continue with email</span></div>
41
+
42
+ <form class="wf-form" id="wf-email-form" novalidate>
43
+ <div class="wf-row-2">
44
+ <div class="wf-field">
45
+ <label class="wf-label" for="wf-first-name">First name</label>
46
+ <input
47
+ type="text"
48
+ id="wf-first-name"
49
+ class="wf-input"
50
+ placeholder="John"
51
+ autocomplete="given-name"
52
+ required
53
+ />
54
+ </div>
55
+ <div class="wf-field">
56
+ <label class="wf-label" for="wf-last-name">Last name</label>
57
+ <input
58
+ type="text"
59
+ id="wf-last-name"
60
+ class="wf-input"
61
+ placeholder="Doe"
62
+ autocomplete="family-name"
63
+ required
64
+ />
65
+ </div>
66
+ </div>
67
+
68
+ <div class="wf-field">
69
+ <label class="wf-label" for="wf-email">Email address</label>
70
+ <div class="wf-input-wrap">
71
+ <input
72
+ type="email"
73
+ id="wf-email"
74
+ class="wf-input"
75
+ placeholder="you@company.com"
76
+ autocomplete="email"
77
+ required
78
+ />
79
+ <span class="wf-input-icon-left">${MAIL_ICON}</span>
80
+ </div>
81
+ </div>
82
+
83
+ <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-full">
84
+ Continue
85
+ <svg class="wf-icon-sm" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
86
+ </button>
87
+ </form>
88
+
89
+ <p class="wf-footer-link">
90
+ Already have an account? <button type="button" class="wf-link" id="wf-go-login">Sign in</button>
91
+ </p>
92
+ `;
93
+
94
+ const form = screen.querySelector('#wf-email-form');
95
+ const firstNameInput = screen.querySelector('#wf-first-name');
96
+ const lastNameInput = screen.querySelector('#wf-last-name');
97
+ const emailInput = screen.querySelector('#wf-email');
98
+
99
+ form.addEventListener('submit', (e) => {
100
+ e.preventDefault();
101
+ const firstName = firstNameInput.value.trim();
102
+ const lastName = lastNameInput.value.trim();
103
+ const email = emailInput.value.trim();
104
+
105
+ if (!firstName || !lastName) {
106
+ firstNameInput.focus();
107
+ return;
108
+ }
109
+ if (!email || !email.includes('@')) {
110
+ emailInput.focus();
111
+ return;
112
+ }
113
+
114
+ onNext({ email, firstName, lastName });
115
+ });
116
+
117
+ const googleBtn = screen.querySelector('#wf-social-google');
118
+ const msBtn = screen.querySelector('#wf-social-ms');
119
+
120
+ if (typeof onSocialSignUp === 'function') {
121
+ googleBtn.addEventListener('click', () => onSocialSignUp('google'));
122
+ msBtn.addEventListener('click', () => onSocialSignUp('microsoft'));
123
+ }
124
+
125
+ const goLoginBtn = screen.querySelector('#wf-go-login');
126
+ if (goLoginBtn && typeof onSwitchToLogin === 'function') {
127
+ goLoginBtn.addEventListener('click', () => onSwitchToLogin());
128
+ }
129
+
130
+ const backBtn = screen.querySelector('#wf-email-back');
131
+ if (backBtn && typeof onBackToApp === 'function') {
132
+ backBtn.addEventListener('click', () => onBackToApp());
133
+ }
134
+
135
+ return screen;
136
+ }