chordia-ui 3.2.2 → 3.2.3

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/style.css CHANGED
@@ -1 +1 @@
1
- :root{--font-sans: "Averta", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-display: "Tomato Grotesk", "Averta", ui-sans-serif, system-ui, sans-serif;--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--default-font-family: var(--font-sans);--default-mono-font-family: var(--font-mono);--bg-chordia: #F4F1E6;--paper: rgba(255, 255, 255, .78);--paper-secondary: rgba(255, 255, 255, .55);--paper-elevated: rgba(255, 255, 255, .82);--paper-high: rgba(255, 255, 255, .85);--text-ink: #1E2125;--text-base: rgba(30, 33, 37, .78);--text-strong: rgba(30, 33, 37, .92);--text-muted: rgba(30, 33, 37, .56);--text-faint: rgba(30, 33, 37, .36);--text-subtle: rgba(30, 33, 37, .52);--text-xfaint: rgba(30, 33, 37, .28);--border: rgba(52, 58, 64, .12);--border-hover: rgba(52, 58, 64, .18);--border-strong: rgba(52, 58, 64, .22);--border-subtle: rgba(52, 58, 64, .08);--hover-warm: rgba(231, 212, 162, .12);--hover-warm-strong: rgba(231, 212, 162, .18);--hover-warm-subtle: rgba(231, 212, 162, .08);--hover-cool: rgba(210, 220, 235, .18);--focus: rgba(231, 212, 162, .55);--state-present: #25A372;--state-absent: rgba(30, 33, 37, .28);--state-unknown: #E7BF33;--rail-compliance: #C98A5A;--rail-orange: #C98A5A;--rail-tone: #9B7AA8;--rail-purple: #9B7AA8;--rail-discovery: #5E88B0;--rail-blue: #5E88B0;--rail-outcome: #6B7C93;--rail-slate: #6B7C93;--rail-signal-churn: #D17B6B;--rail-coral: #D17B6B;--rail-signal-upsell: #7BA89D;--rail-teal: #B8976A;--rail-signal-satisfaction: #9B8E6F;--rail-olive: #9B8E6F;--rail-quality: #8A9BAF;--card-customer: rgba(94, 136, 176, .06);--card-agent: var(--paper-elevated);--card-assistant: rgba(155, 122, 168, .05);--rail-width-thin: 4px;--rail-width: 5px;--rail-width-thick: 6px;--deviation-dot-size: 6px;--deviation-gap: 3px;--timestamp-bg: rgba(255, 255, 255, .7);--timestamp-border: rgba(52, 58, 64, .16);--bar-h: 3px;--bar-w: 45px;--bar-bg: rgba(30, 33, 37, .12);--bar-fill: rgba(30, 33, 37, .45);--tooltip-bg: rgba(30, 33, 37, .95);--tooltip-text: rgba(255, 255, 255, .95);--background: var(--bg-chordia);--foreground: var(--text-ink);--card: var(--paper-elevated);--card-foreground: var(--text-ink);--popover: var(--paper-high);--popover-foreground: var(--text-ink);--primary: #030213;--primary-foreground: #ffffff;--secondary: rgba(249, 250, 251, 1);--secondary-foreground: #030213;--accent: rgba(233, 235, 239, 1);--accent-foreground: #030213;--destructive: #C98A5A;--destructive-foreground: #ffffff;--input: transparent;--input-background: rgba(243, 243, 245, 1);--switch-background: rgba(203, 206, 212, 1);--ring: rgba(113, 113, 122, .5);--chart-1: #25A372;--chart-2: #44C090;--chart-3: #B7D89E;--chart-4: #E7BF33;--chart-5: #C98A5A;--sidebar: var(--paper-high);--sidebar-foreground: var(--text-ink);--sidebar-primary: #030213;--sidebar-primary-foreground: #ffffff;--sidebar-accent: var(--hover-warm-subtle);--sidebar-accent-foreground: var(--text-ink);--sidebar-border: var(--border);--sidebar-ring: var(--ring);--bg: var(--bg-chordia);--paper2: var(--paper-secondary);--ink: var(--text-ink);--text: var(--text-base);--muted: var(--text-muted);--faint: var(--text-faint);--b1: var(--border);--b2: var(--border-hover);--warm: var(--hover-warm);--cool: var(--hover-cool);--present: var(--state-present);--absent: var(--state-absent);--unknown: var(--state-unknown);--s1: var(--shadow-lg);--s2: var(--shadow-md);--r1: var(--radius-lg);--r2: var(--radius-md);--font-size: .875rem;--color-green: #00A66E;--color-green-hover: #009160;--color-green-disabled: #80d3b7;--color-green-ring: rgba(0, 166, 110, .12);--surface-dark: #1D1E20;--surface-card-dark: linear-gradient(180deg, #48473B 0%, #26261F 100%);--surface-card-dark-border: #333A42;--color-card-icon: #000000;--color-card-text: #ffffff;--color-text: #2E3236;--color-text-secondary: #808183;--color-text-inverse: #E8E6E1;--color-text-overlay: rgba(255, 255, 255, .5);--color-input-border: #ACADAD;--color-divider: #E5E7EB;--color-error: #E15448;--color-error-border: #ED8077;--color-error-ring: rgba(237, 128, 119, .12);--color-error-bg: #F3F7F7;--gradient-card-active: linear-gradient(160deg, #4A5330 0%, #252C14 100%);--gradient-card-inactive: linear-gradient(160deg, rgba(74, 83, 48, .25) 0%, rgba(37, 44, 20, .35) 100%)}.custom-thin-scrollbar-library::-webkit-scrollbar{width:4px;background:transparent;border-radius:2px}.custom-thin-scrollbar-library::-webkit-scrollbar-track{background:transparent}.custom-thin-scrollbar-library::-webkit-scrollbar-thumb{background:rgb(180,178,178);border-radius:2px}.custom-thin-scrollbar-library::-webkit-scrollbar-thumb:hover{background:var(--border-hover)}.custom-thin-scrollbar-library-hidden::-webkit-scrollbar{width:0px;background:transparent}.custom-thin-scrollbar-library-hidden::-webkit-scrollbar-track{background:transparent}.custom-thin-scrollbar-library-hidden::-webkit-scrollbar-thumb{background:transparent}.custom-thin-scrollbar-hidden{scrollbar-width:none;-ms-overflow-style:none}.custom-thin-scrollbar-ultra::-webkit-scrollbar{width:2px;background:transparent}.custom-thin-scrollbar-ultra::-webkit-scrollbar-track{background:transparent}.custom-thin-scrollbar-ultra::-webkit-scrollbar-thumb{background:var(--border);border-radius:1px}.custom-thin-scrollbar-ultra::-webkit-scrollbar-thumb:hover{background:var(--border-hover)}.custom-thin-scrollbar-ultra{scrollbar-width:thin;scrollbar-color:var(--border) transparent}
1
+ :root{--font-sans: "Averta", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-display: "Tomato Grotesk", "Averta", ui-sans-serif, system-ui, sans-serif;--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--default-font-family: var(--font-sans);--default-mono-font-family: var(--font-mono);--bg-chordia: #F4F1E6;--paper: rgba(255, 255, 255, .78);--paper-secondary: rgba(255, 255, 255, .55);--paper-elevated: rgba(255, 255, 255, .82);--paper-high: rgba(255, 255, 255, .85);--text-ink: #1E2125;--text-base: rgba(30, 33, 37, .78);--text-strong: rgba(30, 33, 37, .92);--text-muted: rgba(30, 33, 37, .56);--text-faint: rgba(30, 33, 37, .36);--text-subtle: rgba(30, 33, 37, .52);--text-xfaint: rgba(30, 33, 37, .28);--border: rgba(52, 58, 64, .12);--border-hover: rgba(52, 58, 64, .18);--border-strong: rgba(52, 58, 64, .22);--border-subtle: rgba(52, 58, 64, .08);--hover-warm: rgba(231, 212, 162, .12);--hover-warm-strong: rgba(231, 212, 162, .18);--hover-warm-subtle: rgba(231, 212, 162, .08);--hover-cool: rgba(210, 220, 235, .18);--focus: rgba(231, 212, 162, .55);--state-present: #25A372;--state-absent: rgba(30, 33, 37, .28);--state-unknown: #E7BF33;--rail-compliance: #C98A5A;--rail-orange: #C98A5A;--rail-tone: #9B7AA8;--rail-purple: #9B7AA8;--rail-discovery: #5E88B0;--rail-blue: #5E88B0;--rail-outcome: #6B7C93;--rail-slate: #6B7C93;--rail-signal-churn: #D17B6B;--rail-coral: #D17B6B;--rail-signal-upsell: #7BA89D;--rail-teal: #B8976A;--rail-signal-satisfaction: #9B8E6F;--rail-olive: #9B8E6F;--rail-quality: #8A9BAF;--card-customer: rgba(94, 136, 176, .06);--card-agent: var(--paper-elevated);--card-assistant: rgba(155, 122, 168, .05);--rail-width-thin: 4px;--rail-width: 5px;--rail-width-thick: 6px;--deviation-dot-size: 6px;--deviation-gap: 3px;--timestamp-bg: rgba(255, 255, 255, .7);--timestamp-border: rgba(52, 58, 64, .16);--bar-h: 3px;--bar-w: 45px;--bar-bg: rgba(30, 33, 37, .12);--bar-fill: rgba(30, 33, 37, .45);--tooltip-bg: rgba(30, 33, 37, .95);--tooltip-text: rgba(255, 255, 255, .95);--background: var(--bg-chordia);--foreground: var(--text-ink);--card: var(--paper-elevated);--card-foreground: var(--text-ink);--popover: var(--paper-high);--popover-foreground: var(--text-ink);--primary: #030213;--primary-foreground: #ffffff;--secondary: rgba(249, 250, 251, 1);--secondary-foreground: #030213;--accent: rgba(233, 235, 239, 1);--accent-foreground: #030213;--destructive: #C98A5A;--destructive-foreground: #ffffff;--input: transparent;--input-background: rgba(243, 243, 245, 1);--switch-background: rgba(203, 206, 212, 1);--ring: rgba(113, 113, 122, .5);--chart-1: #25A372;--chart-2: #44C090;--chart-3: #B7D89E;--chart-4: #E7BF33;--chart-5: #C98A5A;--sidebar: var(--paper-high);--sidebar-foreground: var(--text-ink);--sidebar-primary: #030213;--sidebar-primary-foreground: #ffffff;--sidebar-accent: var(--hover-warm-subtle);--sidebar-accent-foreground: var(--text-ink);--sidebar-border: var(--border);--sidebar-ring: var(--ring);--bg: var(--bg-chordia);--paper2: var(--paper-secondary);--ink: var(--text-ink);--text: var(--text-base);--muted: var(--text-muted);--faint: var(--text-faint);--b1: var(--border);--b2: var(--border-hover);--warm: var(--hover-warm);--cool: var(--hover-cool);--present: var(--state-present);--absent: var(--state-absent);--unknown: var(--state-unknown);--s1: var(--shadow-lg);--s2: var(--shadow-md);--r1: var(--radius-lg);--r2: var(--radius-md);--font-size: .875rem;--color-green: #00A66E;--color-green-hover: #009160;--color-green-disabled: #80d3b7;--color-green-ring: rgba(0, 166, 110, .12);--surface-dark: #1D1E20;--surface-card-dark: linear-gradient(180deg, #48473B 0%, #26261F 100%);--surface-card-dark-border: #333A42;--color-card-icon: #000000;--color-card-text: #ffffff;--color-text: #2E3236;--color-text-secondary: #808183;--color-text-inverse: #E8E6E1;--color-text-overlay: rgba(255, 255, 255, .5);--color-input-border: #ACADAD;--color-divider: #E5E7EB;--color-error: #E15448;--color-error-border: #ED8077;--color-error-ring: rgba(237, 128, 119, .12);--color-error-bg: #F3F7F7;--grey-white: #FFFFFF;--grey-absent: #D9D9D9;--grey-muted: #BDBDBD;--grey-strong: var(--color-text);--focus-2: #FAF5E9;--gradient-card-active: linear-gradient(160deg, #4A5330 0%, #252C14 100%);--gradient-card-inactive: linear-gradient(160deg, rgba(74, 83, 48, .25) 0%, rgba(37, 44, 20, .35) 100%)}.custom-thin-scrollbar-library::-webkit-scrollbar{width:4px;background:transparent;border-radius:2px}.custom-thin-scrollbar-library::-webkit-scrollbar-track{background:transparent}.custom-thin-scrollbar-library::-webkit-scrollbar-thumb{background:rgb(180,178,178);border-radius:2px}.custom-thin-scrollbar-library::-webkit-scrollbar-thumb:hover{background:var(--border-hover)}.custom-thin-scrollbar-library-hidden::-webkit-scrollbar{width:0px;background:transparent}.custom-thin-scrollbar-library-hidden::-webkit-scrollbar-track{background:transparent}.custom-thin-scrollbar-library-hidden::-webkit-scrollbar-thumb{background:transparent}.custom-thin-scrollbar-hidden{scrollbar-width:none;-ms-overflow-style:none}.custom-thin-scrollbar-ultra::-webkit-scrollbar{width:2px;background:transparent}.custom-thin-scrollbar-ultra::-webkit-scrollbar-track{background:transparent}.custom-thin-scrollbar-ultra::-webkit-scrollbar-thumb{background:var(--border);border-radius:1px}.custom-thin-scrollbar-ultra::-webkit-scrollbar-thumb:hover{background:var(--border-hover)}.custom-thin-scrollbar-ultra{scrollbar-width:thin;scrollbar-color:var(--border) transparent}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chordia-ui",
3
- "version": "3.2.2",
3
+ "version": "3.2.3",
4
4
  "description": "Chordia Design System - UI components, tokens, and Tailwind preset",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
@@ -162,6 +162,8 @@ export default function LoginPage({
162
162
  }) {
163
163
  const [email, setEmail] = useState('');
164
164
  const [password, setPassword] = useState('');
165
+ const [emailAutofilled, setEmailAutofilled] = useState(false);
166
+ const [passwordAutofilled, setPasswordAutofilled] = useState(false);
165
167
  const [showPassword, setShowPassword] = useState(false);
166
168
  const [internalLoading, setInternalLoading] = useState(false);
167
169
  const [internalError, setInternalError] = useState(null);
@@ -190,13 +192,24 @@ export default function LoginPage({
190
192
  // Sync React state with browser autofill (autofill doesn't fire onChange)
191
193
  const emailRef = useRef(null);
192
194
  const passwordRef = useRef(null);
193
- const [autofilled, setAutofilled] = useState(false);
194
195
  useEffect(() => {
196
+ const isAutofilled = (el) => {
197
+ if (!el) return false;
198
+ try {
199
+ return el.matches(':-webkit-autofill, :autofill');
200
+ } catch {
201
+ // Some browsers may throw for unsupported pseudo-class selectors.
202
+ return false;
203
+ }
204
+ };
205
+
195
206
  const sync = () => {
196
207
  const e = emailRef.current;
197
208
  const p = passwordRef.current;
198
209
  if (e && e.value) setEmail(e.value);
199
210
  if (p && p.value) setPassword(p.value);
211
+ setEmailAutofilled(isAutofilled(e));
212
+ setPasswordAutofilled(isAutofilled(p));
200
213
  };
201
214
  // Poll for autofill values at multiple intervals
202
215
  const timers = [100, 300, 600, 1000, 2000].map(ms => setTimeout(sync, ms));
@@ -204,7 +217,6 @@ export default function LoginPage({
204
217
  // Detect Chrome/Safari autofill via the :-webkit-autofill animation
205
218
  const handleAnimationStart = (e) => {
206
219
  if (e.animationName === 'onAutoFillStart' || e.target.matches(':-webkit-autofill')) {
207
- setAutofilled(true);
208
220
  setTimeout(sync, 50);
209
221
  }
210
222
  };
@@ -212,10 +224,13 @@ export default function LoginPage({
212
224
  const pEl = passwordRef.current;
213
225
  eEl?.addEventListener('animationstart', handleAnimationStart);
214
226
  pEl?.addEventListener('animationstart', handleAnimationStart);
215
- // Also listen for input/change events that some password managers fire
227
+ // Listen for events commonly used by password managers/autofill
216
228
  const onInput = () => setTimeout(sync, 0);
229
+ const onChange = () => setTimeout(sync, 0);
217
230
  eEl?.addEventListener('input', onInput);
218
231
  pEl?.addEventListener('input', onInput);
232
+ eEl?.addEventListener('change', onChange);
233
+ pEl?.addEventListener('change', onChange);
219
234
 
220
235
  return () => {
221
236
  timers.forEach(clearTimeout);
@@ -223,9 +238,43 @@ export default function LoginPage({
223
238
  pEl?.removeEventListener('animationstart', handleAnimationStart);
224
239
  eEl?.removeEventListener('input', onInput);
225
240
  pEl?.removeEventListener('input', onInput);
241
+ eEl?.removeEventListener('change', onChange);
242
+ pEl?.removeEventListener('change', onChange);
226
243
  };
227
244
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
228
245
 
246
+ useEffect(() => {
247
+ const isAutofilled = (el) => {
248
+ if (!el) return false;
249
+ try {
250
+ return el.matches(':-webkit-autofill, :autofill');
251
+ } catch {
252
+ return false;
253
+ }
254
+ };
255
+
256
+ const syncIfNeeded = () => {
257
+ const nextEmail = emailRef.current?.value || '';
258
+ const nextPassword = passwordRef.current?.value || '';
259
+
260
+ if (nextEmail && nextEmail !== email) setEmail(nextEmail);
261
+ if (nextPassword && nextPassword !== password) setPassword(nextPassword);
262
+ setEmailAutofilled(isAutofilled(emailRef.current));
263
+ setPasswordAutofilled(isAutofilled(passwordRef.current));
264
+ };
265
+
266
+ // Some browsers/password managers update input values without firing events.
267
+ const intervalId = setInterval(syncIfNeeded, 250);
268
+ window.addEventListener('focus', syncIfNeeded);
269
+ document.addEventListener('visibilitychange', syncIfNeeded);
270
+
271
+ return () => {
272
+ clearInterval(intervalId);
273
+ window.removeEventListener('focus', syncIfNeeded);
274
+ document.removeEventListener('visibilitychange', syncIfNeeded);
275
+ };
276
+ }, [email, password]);
277
+
229
278
  // Switch to full-name screen when host app signals unregistered email
230
279
  useEffect(() => {
231
280
  if (codeError && view === 'verifycode') {
@@ -236,7 +285,12 @@ export default function LoginPage({
236
285
 
237
286
  const loading = externalLoading ?? internalLoading;
238
287
  const error = externalError ?? internalError;
239
- const canSubmit = (email && password && !loading) || (autofilled && !loading);
288
+ const currentEmail = (email || emailRef.current?.value || '').trim();
289
+ const currentPassword = password || passwordRef.current?.value || '';
290
+ const hasEmail = Boolean(currentEmail || emailAutofilled);
291
+ const hasPassword = Boolean(currentPassword || passwordAutofilled);
292
+ const canSubmit = Boolean(hasEmail && hasPassword && !loading);
293
+ const otpError = (view === 'verifycode' || view === 'getcodewithname') ? error : null;
240
294
 
241
295
  // Error-aware focus handlers for sign-in fields
242
296
  const focusErr = (e) => {
@@ -248,8 +302,32 @@ export default function LoginPage({
248
302
  e.target.style.boxShadow = 'none';
249
303
  };
250
304
 
305
+ const applyOtpFromString = (startIndex, rawValue) => {
306
+ const clean = (rawValue || '').replace(/\D/g, '');
307
+ if (!clean) return;
308
+
309
+ const next = [...otpDigits];
310
+ let lastFilledIndex = startIndex;
311
+
312
+ for (let i = 0; i < clean.length && startIndex + i < next.length; i += 1) {
313
+ next[startIndex + i] = clean[i];
314
+ lastFilledIndex = startIndex + i;
315
+ }
316
+
317
+ setOtpDigits(next);
318
+
319
+ const nextIndex = Math.min(lastFilledIndex + 1, next.length - 1);
320
+ otpRefs.current[nextIndex]?.focus();
321
+ };
322
+
251
323
  const handleOtpChange = (index, value) => {
252
- const digit = value.replace(/\D/g, '').slice(-1);
324
+ const clean = value.replace(/\D/g, '');
325
+ if (clean.length > 1) {
326
+ applyOtpFromString(index, clean);
327
+ return;
328
+ }
329
+
330
+ const digit = clean.slice(-1);
253
331
  const next = [...otpDigits];
254
332
  next[index] = digit;
255
333
  setOtpDigits(next);
@@ -262,6 +340,12 @@ export default function LoginPage({
262
340
  }
263
341
  };
264
342
 
343
+ const handleOtpPaste = (index, e) => {
344
+ e.preventDefault();
345
+ const pasted = e.clipboardData?.getData('text') || '';
346
+ applyOtpFromString(index, pasted);
347
+ };
348
+
265
349
  const handleSubmit = async (e) => {
266
350
  e.preventDefault();
267
351
  if (!canSubmit) return;
@@ -521,6 +605,7 @@ export default function LoginPage({
521
605
  type="text" inputMode="numeric" maxLength={1} value={digit}
522
606
  onChange={(e) => handleOtpChange(i, e.target.value)}
523
607
  onKeyDown={(e) => handleOtpKeyDown(i, e)}
608
+ onPaste={(e) => handleOtpPaste(i, e)}
524
609
  autoFocus={i === 0}
525
610
  style={{
526
611
  flex: 1, minWidth: 0, height: 40, textAlign: 'center', fontSize: 16, fontWeight: 600,
@@ -537,6 +622,14 @@ export default function LoginPage({
537
622
  <GreenButton onClick={() => onVerifyCode?.(verifyEmail, otpDigits.join(''))} disabled={otpDigits.some(d => !d)}>
538
623
  Verify Code
539
624
  </GreenButton>
625
+ {otpError && (
626
+ <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', padding: 8, gap: 6, width: '100%', boxSizing: 'border-box', background: 'var(--color-error-bg)', borderRadius: 5, fontSize: 15, fontWeight: 400, lineHeight: '22px', color: 'var(--color-text)', fontFamily: FF }}>
627
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="var(--color-text)" style={{ flexShrink: 0 }}>
628
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
629
+ </svg>
630
+ {otpError}
631
+ </div>
632
+ )}
540
633
  <Divider />
541
634
  <NavRow text="Not received yet?" linkText="Resend" onClick={() => onResendCode?.(verifyEmail)} />
542
635
  <TermsFooter onTerms={onTerms} onPrivacyPolicy={onPrivacyPolicy} paddingTop={40} />
@@ -588,6 +681,7 @@ export default function LoginPage({
588
681
  type="text" inputMode="numeric" maxLength={1} value={digit}
589
682
  onChange={(e) => handleOtpChange(i, e.target.value)}
590
683
  onKeyDown={(e) => handleOtpKeyDown(i, e)}
684
+ onPaste={(e) => handleOtpPaste(i, e)}
591
685
  autoFocus={i === 0}
592
686
  style={{
593
687
  flex: 1, minWidth: 0, height: 40, textAlign: 'center', fontSize: 16, fontWeight: 600,
@@ -604,6 +698,14 @@ export default function LoginPage({
604
698
  <GreenButton onClick={() => { setVerifyEmail(codeEmail); onOneTimeCode?.(codeEmail, codeFullName, otpDigits.join('')); }} disabled={!codeEmail || !codeFullName || otpDigits.some(d => !d)}>
605
699
  Verify & Sign In
606
700
  </GreenButton>
701
+ {otpError && (
702
+ <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', padding: 8, gap: 6, width: '100%', boxSizing: 'border-box', background: 'var(--color-error-bg)', borderRadius: 5, fontSize: 15, fontWeight: 400, lineHeight: '22px', color: 'var(--color-text)', fontFamily: FF }}>
703
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="var(--color-text)" style={{ flexShrink: 0 }}>
704
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
705
+ </svg>
706
+ {otpError}
707
+ </div>
708
+ )}
607
709
  <Divider />
608
710
  <NavRow text="I have my password" linkText="Back to Sign in" onClick={() => { setView('signin'); onGoToLogin?.(); }} />
609
711
  <TermsFooter onTerms={onTerms} onPrivacyPolicy={onPrivacyPolicy} paddingTop={40} />
@@ -0,0 +1,89 @@
1
+ import IntegrationCard from '../layout/IntegrationCard';
2
+
3
+ const FF = 'var(--font-sans)';
4
+
5
+ // ─── Styles ───
6
+
7
+ const containerStyle = {
8
+ fontFamily: FF,
9
+ };
10
+
11
+ const sectionTitleStyle = {
12
+ fontSize: 20,
13
+ fontWeight: 600,
14
+ fontStyle: 'normal',
15
+ fontFamily: FF,
16
+ color: 'var(--grey-strong)',
17
+ margin: 0,
18
+ lineHeight: 'normal',
19
+ };
20
+
21
+ const sectionSubtitleStyle = {
22
+ fontSize: 13,
23
+ fontWeight: 400,
24
+ fontStyle: 'normal',
25
+ color: 'var(--color-text-secondary)',
26
+ fontFamily: FF,
27
+ margin: '4px 0 0',
28
+ lineHeight: '140%',
29
+ };
30
+
31
+ const gridStyle = {
32
+ display: 'grid',
33
+ gridTemplateColumns: 'repeat(3, 1fr)',
34
+ gap: 16,
35
+ marginTop: 24,
36
+ };
37
+
38
+ // ─── Default Integrations ───
39
+
40
+ const DEFAULT_INTEGRATIONS = [
41
+ {
42
+ providerName: 'Five9',
43
+ description: 'Cloud contact center platform for voice and digital channels',
44
+ status: 'connected',
45
+ railColor: '#5E88B0',
46
+ },
47
+ {
48
+ providerName: 'Twilio Flex',
49
+ description: 'Programmable contact center with custom workflows',
50
+ status: 'available',
51
+ railColor: '#9B7AA8',
52
+ },
53
+ {
54
+ providerName: 'Zoom Phone',
55
+ description: 'Cloud phone system with recording capabilities',
56
+ status: 'coming-soon',
57
+ railColor: '#6B7C93',
58
+ },
59
+ ];
60
+
61
+ // ─── Component ───
62
+
63
+ const ConnectData = ({ integrations = DEFAULT_INTEGRATIONS, onConfigure }) => {
64
+ return (
65
+ <div style={containerStyle}>
66
+ <h2 style={sectionTitleStyle}>Connect Data Source</h2>
67
+ <p style={sectionSubtitleStyle}>
68
+ Choose your preferred platforms to synchronise data.
69
+ </p>
70
+
71
+ <div style={gridStyle}>
72
+ {integrations.map((integration) => (
73
+ <IntegrationCard
74
+ key={integration.providerName}
75
+ providerName={integration.providerName}
76
+ description={integration.description}
77
+ status={integration.status}
78
+ railColor={integration.railColor}
79
+ logoUrl={integration.logoUrl}
80
+ icon={integration.icon}
81
+ onConfigure={() => onConfigure?.(integration)}
82
+ />
83
+ ))}
84
+ </div>
85
+ </div>
86
+ );
87
+ };
88
+
89
+ export default ConnectData;