chordia-ui 3.2.0 → 3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chordia-ui",
3
- "version": "3.2.0",
3
+ "version": "3.2.2",
4
4
  "description": "Chordia Design System - UI components, tokens, and Tailwind preset",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
@@ -157,6 +157,7 @@ export default function LoginPage({
157
157
  onGoToLogin,
158
158
  loading: externalLoading,
159
159
  error: externalError,
160
+ codeError,
160
161
  defaultView = 'signin',
161
162
  }) {
162
163
  const [email, setEmail] = useState('');
@@ -166,8 +167,9 @@ export default function LoginPage({
166
167
  const [internalError, setInternalError] = useState(null);
167
168
  const [view, setView] = useState(defaultView);
168
169
  const [codeEmail, setCodeEmail] = useState('');
170
+ const [codeFullName, setCodeFullName] = useState('');
169
171
  const [verifyEmail, setVerifyEmail] = useState('');
170
- const [otpDigits, setOtpDigits] = useState(['', '', '', '']);
172
+ const [otpDigits, setOtpDigits] = useState(['', '', '', '', '', '']);
171
173
  const otpRefs = useRef([]);
172
174
  const [firstName, setFirstName] = useState('');
173
175
  const [lastName, setLastName] = useState('');
@@ -185,9 +187,56 @@ export default function LoginPage({
185
187
  return () => clearInterval(t);
186
188
  }, []);
187
189
 
190
+ // Sync React state with browser autofill (autofill doesn't fire onChange)
191
+ const emailRef = useRef(null);
192
+ const passwordRef = useRef(null);
193
+ const [autofilled, setAutofilled] = useState(false);
194
+ useEffect(() => {
195
+ const sync = () => {
196
+ const e = emailRef.current;
197
+ const p = passwordRef.current;
198
+ if (e && e.value) setEmail(e.value);
199
+ if (p && p.value) setPassword(p.value);
200
+ };
201
+ // Poll for autofill values at multiple intervals
202
+ const timers = [100, 300, 600, 1000, 2000].map(ms => setTimeout(sync, ms));
203
+
204
+ // Detect Chrome/Safari autofill via the :-webkit-autofill animation
205
+ const handleAnimationStart = (e) => {
206
+ if (e.animationName === 'onAutoFillStart' || e.target.matches(':-webkit-autofill')) {
207
+ setAutofilled(true);
208
+ setTimeout(sync, 50);
209
+ }
210
+ };
211
+ const eEl = emailRef.current;
212
+ const pEl = passwordRef.current;
213
+ eEl?.addEventListener('animationstart', handleAnimationStart);
214
+ pEl?.addEventListener('animationstart', handleAnimationStart);
215
+ // Also listen for input/change events that some password managers fire
216
+ const onInput = () => setTimeout(sync, 0);
217
+ eEl?.addEventListener('input', onInput);
218
+ pEl?.addEventListener('input', onInput);
219
+
220
+ return () => {
221
+ timers.forEach(clearTimeout);
222
+ eEl?.removeEventListener('animationstart', handleAnimationStart);
223
+ pEl?.removeEventListener('animationstart', handleAnimationStart);
224
+ eEl?.removeEventListener('input', onInput);
225
+ pEl?.removeEventListener('input', onInput);
226
+ };
227
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
228
+
229
+ // Switch to full-name screen when host app signals unregistered email
230
+ useEffect(() => {
231
+ if (codeError && view === 'verifycode') {
232
+ setOtpDigits(['', '', '', '', '', '']);
233
+ setView('getcodewithname');
234
+ }
235
+ }, [codeError]); // eslint-disable-line react-hooks/exhaustive-deps
236
+
188
237
  const loading = externalLoading ?? internalLoading;
189
238
  const error = externalError ?? internalError;
190
- const canSubmit = email && password && !loading;
239
+ const canSubmit = (email && password && !loading) || (autofilled && !loading);
191
240
 
192
241
  // Error-aware focus handlers for sign-in fields
193
242
  const focusErr = (e) => {
@@ -204,7 +253,7 @@ export default function LoginPage({
204
253
  const next = [...otpDigits];
205
254
  next[index] = digit;
206
255
  setOtpDigits(next);
207
- if (digit && index < 3) otpRefs.current[index + 1]?.focus();
256
+ if (digit && index < 5) otpRefs.current[index + 1]?.focus();
208
257
  };
209
258
 
210
259
  const handleOtpKeyDown = (index, e) => {
@@ -216,10 +265,14 @@ export default function LoginPage({
216
265
  const handleSubmit = async (e) => {
217
266
  e.preventDefault();
218
267
  if (!canSubmit) return;
268
+ // Read directly from DOM if React state missed autofill
269
+ const submitEmail = email || emailRef.current?.value || '';
270
+ const submitPassword = password || passwordRef.current?.value || '';
271
+ if (!submitEmail || !submitPassword) return;
219
272
  setInternalError(null);
220
273
  setInternalLoading(true);
221
274
  try {
222
- await onLogin?.(email, password);
275
+ await onLogin?.(submitEmail, submitPassword);
223
276
  } catch (err) {
224
277
  setInternalError(err?.message || 'Something went wrong. Please try again.');
225
278
  } finally {
@@ -462,7 +515,7 @@ export default function LoginPage({
462
515
 
463
516
  <div style={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 16 }}>
464
517
  <Field label="Enter one-time code" gap={12}>
465
- <div style={{ display: 'flex', gap: 12 }}>
518
+ <div style={{ display: 'flex', gap: 8 }}>
466
519
  {otpDigits.map((digit, i) => (
467
520
  <input key={i} ref={(el) => { otpRefs.current[i] = el; }}
468
521
  type="text" inputMode="numeric" maxLength={1} value={digit}
@@ -470,9 +523,9 @@ export default function LoginPage({
470
523
  onKeyDown={(e) => handleOtpKeyDown(i, e)}
471
524
  autoFocus={i === 0}
472
525
  style={{
473
- width: 72, height: 72, textAlign: 'center', fontSize: 24, fontWeight: 600,
526
+ flex: 1, minWidth: 0, height: 40, textAlign: 'center', fontSize: 16, fontWeight: 600,
474
527
  fontFamily: FF, color: 'var(--color-text)', border: '1px solid var(--color-input-border)',
475
- borderRadius: 8, outline: 'none', background: 'white', boxSizing: 'border-box',
528
+ borderRadius: 4, outline: 'none', background: 'white', boxSizing: 'border-box',
476
529
  transition: 'border-color 0.15s, box-shadow 0.15s', caretColor: GREEN,
477
530
  }}
478
531
  onFocus={focusGreen} onBlur={blurGray}
@@ -491,7 +544,7 @@ export default function LoginPage({
491
544
  </>
492
545
 
493
546
  ) : view === 'getcode' ? (
494
- /* ── Get Code ── */
547
+ /* ── Get Code (original — email only) ── */
495
548
  <>
496
549
  <ViewHeading title="Get code" subtitle="We'll send a one time pass-code to your email" />
497
550
  <div style={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 16 }}>
@@ -501,7 +554,7 @@ export default function LoginPage({
501
554
  style={inputBase} onFocus={focusGreen} onBlur={blurGray} />
502
555
  </Field>
503
556
 
504
- <GreenButton onClick={() => { setVerifyEmail(codeEmail); setOtpDigits(['', '', '', '']); setView('verifycode'); onOneTimeCode?.(codeEmail); }} disabled={!codeEmail}>
557
+ <GreenButton onClick={() => { setVerifyEmail(codeEmail); setOtpDigits(['', '', '', '', '', '']); setView('verifycode'); onOneTimeCode?.(codeEmail); }} disabled={!codeEmail}>
505
558
  Send One-time Code
506
559
  </GreenButton>
507
560
  <Divider />
@@ -510,6 +563,53 @@ export default function LoginPage({
510
563
  </div>
511
564
  </>
512
565
 
566
+ ) : view === 'getcodewithname' ? (
567
+ /* ── Get Code with Full Name (unregistered email) ── */
568
+ <>
569
+ <ViewHeading title="Complete your sign in" subtitle="We’ve sent a one-time code to your email. Please enter the code and your full name to sign in" />
570
+ <div style={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 16 }}>
571
+
572
+ <Field label="Email" gap={12}>
573
+ <input type="email" value={codeEmail} onChange={(e) => setCodeEmail(e.target.value)}
574
+ placeholder="workemail@example.com" autoComplete="email"
575
+ style={inputBase} onFocus={focusGreen} onBlur={blurGray} />
576
+ </Field>
577
+
578
+ <Field label="Full Name" gap={12}>
579
+ <input type="text" value={codeFullName} onChange={(e) => setCodeFullName(e.target.value)}
580
+ placeholder="Enter your full name" autoComplete="name"
581
+ style={inputBase} onFocus={focusGreen} onBlur={blurGray} />
582
+ </Field>
583
+
584
+ <Field label="Enter one-time code" gap={12}>
585
+ <div style={{ display: 'flex', gap: 8 }}>
586
+ {otpDigits.map((digit, i) => (
587
+ <input key={i} ref={(el) => { otpRefs.current[i] = el; }}
588
+ type="text" inputMode="numeric" maxLength={1} value={digit}
589
+ onChange={(e) => handleOtpChange(i, e.target.value)}
590
+ onKeyDown={(e) => handleOtpKeyDown(i, e)}
591
+ autoFocus={i === 0}
592
+ style={{
593
+ flex: 1, minWidth: 0, height: 40, textAlign: 'center', fontSize: 16, fontWeight: 600,
594
+ fontFamily: FF, color: 'var(--color-text)', border: '1px solid var(--color-input-border)',
595
+ borderRadius: 4, outline: 'none', background: 'white', boxSizing: 'border-box',
596
+ transition: 'border-color 0.15s, box-shadow 0.15s', caretColor: GREEN,
597
+ }}
598
+ onFocus={focusGreen} onBlur={blurGray}
599
+ />
600
+ ))}
601
+ </div>
602
+ </Field>
603
+
604
+ <GreenButton onClick={() => { setVerifyEmail(codeEmail); onOneTimeCode?.(codeEmail, codeFullName, otpDigits.join('')); }} disabled={!codeEmail || !codeFullName || otpDigits.some(d => !d)}>
605
+ Verify & Sign In
606
+ </GreenButton>
607
+ <Divider />
608
+ <NavRow text="I have my password" linkText="Back to Sign in" onClick={() => { setView('signin'); onGoToLogin?.(); }} />
609
+ <TermsFooter onTerms={onTerms} onPrivacyPolicy={onPrivacyPolicy} paddingTop={40} />
610
+ </div>
611
+ </>
612
+
513
613
  ) : view === 'resetpassword' ? (
514
614
  /* ── Reset Password ── */
515
615
  <>
@@ -539,7 +639,7 @@ export default function LoginPage({
539
639
 
540
640
  <form onSubmit={handleSubmit} style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 16 }}>
541
641
  <Field label="Email" gap={12}>
542
- <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
642
+ <input ref={emailRef} type="email" value={email} onChange={(e) => setEmail(e.target.value)}
543
643
  placeholder="workemail@example.com" required autoComplete="email"
544
644
  style={{ ...inputBase, border: `1px solid ${error ? 'var(--color-text)' : 'var(--color-input-border)'}` }}
545
645
  onFocus={focusErr} onBlur={blurErr} />
@@ -547,7 +647,7 @@ export default function LoginPage({
547
647
 
548
648
  <Field label="Password" gap={12}>
549
649
  <div style={{ position: 'relative' }}>
550
- <input type={showPassword ? 'text' : 'password'} value={password}
650
+ <input ref={passwordRef} type={showPassword ? 'text' : 'password'} value={password}
551
651
  onChange={(e) => setPassword(e.target.value)}
552
652
  placeholder="Enter password" required autoComplete="current-password"
553
653
  style={{ ...inputBase, padding: '10px 44px 10px 16px', border: `1px solid ${error ? 'var(--color-text)' : 'var(--color-input-border)'}` }}
@@ -588,7 +688,7 @@ export default function LoginPage({
588
688
  </div>
589
689
 
590
690
  <NavRow text="Not a member yet?" linkText="Sign Up" onClick={() => { setView('signup'); onSignUp?.(); }} />
591
- <TermsFooter onTerms={onTerms} onPrivacyPolicy={onPrivacyPolicy} paddingTop={6} />
691
+ <TermsFooter onTerms={onTerms} onPrivacyPolicy={onPrivacyPolicy} paddingTop={16} />
592
692
  </div>
593
693
  </div>
594
694
  )}