@sneekin/ui 0.2.1 → 0.3.0

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/index.js CHANGED
@@ -20,10 +20,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ COUNTRIES: () => COUNTRIES,
24
+ DEFAULT_COUNTRY_ISO2: () => DEFAULT_COUNTRY_ISO2,
23
25
  SNEEK_ACCENT_DARK: () => SNEEK_ACCENT_DARK,
24
26
  SNEEK_ACCENT_LIGHT: () => SNEEK_ACCENT_LIGHT,
25
27
  SneekOtpLogin: () => SneekOtpLogin,
28
+ countryByDialPrefix: () => countryByDialPrefix,
29
+ countryByIso2: () => countryByIso2,
26
30
  createFetchHandlers: () => createFetchHandlers,
31
+ detectCountryFromLocale: () => detectCountryFromLocale,
32
+ flagEmoji: () => flagEmoji,
27
33
  formatChannels: () => formatChannels,
28
34
  initialOtpState: () => initialOtpState,
29
35
  otpReducer: () => otpReducer,
@@ -169,8 +175,340 @@ function useSneekOtp(handlers) {
169
175
  }
170
176
 
171
177
  // src/component.tsx
178
+ var import_react3 = require("react");
179
+
180
+ // src/country-picker.tsx
172
181
  var import_react2 = require("react");
173
182
 
183
+ // src/countries.ts
184
+ function flagFromIso2(iso2) {
185
+ const A = 127462;
186
+ const codePoints = [...iso2.toUpperCase()].map((c) => A + (c.charCodeAt(0) - 65));
187
+ return String.fromCodePoint(...codePoints);
188
+ }
189
+ var RAW = [
190
+ ["US", "United States", "1"],
191
+ ["CA", "Canada", "1"],
192
+ ["GB", "United Kingdom", "44"],
193
+ ["IN", "India", "91"],
194
+ ["AU", "Australia", "61"],
195
+ ["DE", "Germany", "49"],
196
+ ["FR", "France", "33"],
197
+ ["IT", "Italy", "39"],
198
+ ["ES", "Spain", "34"],
199
+ ["PT", "Portugal", "351"],
200
+ ["NL", "Netherlands", "31"],
201
+ ["BE", "Belgium", "32"],
202
+ ["CH", "Switzerland", "41"],
203
+ ["AT", "Austria", "43"],
204
+ ["SE", "Sweden", "46"],
205
+ ["NO", "Norway", "47"],
206
+ ["DK", "Denmark", "45"],
207
+ ["FI", "Finland", "358"],
208
+ ["IE", "Ireland", "353"],
209
+ ["IS", "Iceland", "354"],
210
+ ["PL", "Poland", "48"],
211
+ ["CZ", "Czechia", "420"],
212
+ ["SK", "Slovakia", "421"],
213
+ ["HU", "Hungary", "36"],
214
+ ["RO", "Romania", "40"],
215
+ ["BG", "Bulgaria", "359"],
216
+ ["GR", "Greece", "30"],
217
+ ["TR", "Turkiye", "90"],
218
+ ["RU", "Russia", "7"],
219
+ ["UA", "Ukraine", "380"],
220
+ ["BY", "Belarus", "375"],
221
+ ["LT", "Lithuania", "370"],
222
+ ["LV", "Latvia", "371"],
223
+ ["EE", "Estonia", "372"],
224
+ ["HR", "Croatia", "385"],
225
+ ["RS", "Serbia", "381"],
226
+ ["SI", "Slovenia", "386"],
227
+ ["CY", "Cyprus", "357"],
228
+ ["MT", "Malta", "356"],
229
+ ["LU", "Luxembourg", "352"],
230
+ ["CN", "China", "86"],
231
+ ["JP", "Japan", "81"],
232
+ ["KR", "South Korea", "82"],
233
+ ["HK", "Hong Kong", "852"],
234
+ ["TW", "Taiwan", "886"],
235
+ ["MO", "Macau", "853"],
236
+ ["SG", "Singapore", "65"],
237
+ ["MY", "Malaysia", "60"],
238
+ ["ID", "Indonesia", "62"],
239
+ ["TH", "Thailand", "66"],
240
+ ["VN", "Vietnam", "84"],
241
+ ["PH", "Philippines", "63"],
242
+ ["PK", "Pakistan", "92"],
243
+ ["BD", "Bangladesh", "880"],
244
+ ["LK", "Sri Lanka", "94"],
245
+ ["NP", "Nepal", "977"],
246
+ ["MM", "Myanmar", "95"],
247
+ ["KH", "Cambodia", "855"],
248
+ ["LA", "Laos", "856"],
249
+ ["MN", "Mongolia", "976"],
250
+ ["KZ", "Kazakhstan", "7"],
251
+ ["UZ", "Uzbekistan", "998"],
252
+ ["AE", "United Arab Emirates", "971"],
253
+ ["SA", "Saudi Arabia", "966"],
254
+ ["QA", "Qatar", "974"],
255
+ ["KW", "Kuwait", "965"],
256
+ ["BH", "Bahrain", "973"],
257
+ ["OM", "Oman", "968"],
258
+ ["JO", "Jordan", "962"],
259
+ ["LB", "Lebanon", "961"],
260
+ ["IL", "Israel", "972"],
261
+ ["IQ", "Iraq", "964"],
262
+ ["IR", "Iran", "98"],
263
+ ["EG", "Egypt", "20"],
264
+ ["ZA", "South Africa", "27"],
265
+ ["NG", "Nigeria", "234"],
266
+ ["KE", "Kenya", "254"],
267
+ ["GH", "Ghana", "233"],
268
+ ["ET", "Ethiopia", "251"],
269
+ ["TZ", "Tanzania", "255"],
270
+ ["UG", "Uganda", "256"],
271
+ ["MA", "Morocco", "212"],
272
+ ["DZ", "Algeria", "213"],
273
+ ["TN", "Tunisia", "216"],
274
+ ["BR", "Brazil", "55"],
275
+ ["MX", "Mexico", "52"],
276
+ ["AR", "Argentina", "54"],
277
+ ["CL", "Chile", "56"],
278
+ ["CO", "Colombia", "57"],
279
+ ["PE", "Peru", "51"],
280
+ ["VE", "Venezuela", "58"],
281
+ ["EC", "Ecuador", "593"],
282
+ ["UY", "Uruguay", "598"],
283
+ ["PY", "Paraguay", "595"],
284
+ ["BO", "Bolivia", "591"],
285
+ ["CR", "Costa Rica", "506"],
286
+ ["PA", "Panama", "507"],
287
+ ["DO", "Dominican Republic", "1"],
288
+ ["GT", "Guatemala", "502"],
289
+ ["NZ", "New Zealand", "64"],
290
+ ["FJ", "Fiji", "679"]
291
+ ];
292
+ var COUNTRIES = RAW.map(([iso2, name, dial]) => ({
293
+ iso2,
294
+ name,
295
+ dial
296
+ }));
297
+ var DEFAULT_COUNTRY_ISO2 = "IN";
298
+ function flagEmoji(iso2) {
299
+ return flagFromIso2(iso2);
300
+ }
301
+ function countryByIso2(iso2) {
302
+ return COUNTRIES.find((c) => c.iso2 === iso2);
303
+ }
304
+ function countryByDialPrefix(digits) {
305
+ for (const len of [3, 2, 1]) {
306
+ const prefix = digits.slice(0, len);
307
+ if (prefix.length < len) continue;
308
+ const match = COUNTRIES.find((c) => c.dial === prefix);
309
+ if (match) return match;
310
+ }
311
+ return void 0;
312
+ }
313
+ function detectCountryFromLocale() {
314
+ if (typeof navigator === "undefined") return void 0;
315
+ const locales = navigator.languages ?? [navigator.language];
316
+ for (const locale of locales) {
317
+ const match = /-([A-Z]{2})$/.exec(locale ?? "");
318
+ if (match) {
319
+ const found = countryByIso2(match[1]);
320
+ if (found) return found;
321
+ }
322
+ }
323
+ return void 0;
324
+ }
325
+
326
+ // src/country-picker.tsx
327
+ var import_jsx_runtime = require("react/jsx-runtime");
328
+ function ChevronDown() {
329
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "10", height: "10", viewBox: "0 0 10 10", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
330
+ "path",
331
+ {
332
+ d: "M2 3.5 5 6.5 8 3.5",
333
+ stroke: "currentColor",
334
+ strokeWidth: "1.4",
335
+ strokeLinecap: "round",
336
+ strokeLinejoin: "round"
337
+ }
338
+ ) });
339
+ }
340
+ function CountryPicker({ country, onSelect, disabled }) {
341
+ const [open, setOpen] = (0, import_react2.useState)(false);
342
+ const [query, setQuery] = (0, import_react2.useState)("");
343
+ const rootRef = (0, import_react2.useRef)(null);
344
+ const searchRef = (0, import_react2.useRef)(null);
345
+ const uid = (0, import_react2.useId)();
346
+ (0, import_react2.useEffect)(() => {
347
+ if (!open) return;
348
+ searchRef.current?.focus();
349
+ const onDocPointerDown = (event) => {
350
+ if (rootRef.current && !rootRef.current.contains(event.target)) {
351
+ setOpen(false);
352
+ }
353
+ };
354
+ const onKeyDown = (event) => {
355
+ if (event.key === "Escape") setOpen(false);
356
+ };
357
+ document.addEventListener("mousedown", onDocPointerDown);
358
+ document.addEventListener("keydown", onKeyDown);
359
+ return () => {
360
+ document.removeEventListener("mousedown", onDocPointerDown);
361
+ document.removeEventListener("keydown", onKeyDown);
362
+ };
363
+ }, [open]);
364
+ const q = query.trim().toLowerCase();
365
+ const filtered = q ? COUNTRIES.filter(
366
+ (c) => c.name.toLowerCase().includes(q) || c.dial.includes(q.replace(/^\+/, ""))
367
+ ) : COUNTRIES;
368
+ const listId = `${uid}-country-list`;
369
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref: rootRef, style: { position: "relative", flexShrink: 0 }, children: [
370
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
371
+ "button",
372
+ {
373
+ type: "button",
374
+ className: "sneek-otp-country-chip",
375
+ onClick: () => setOpen((o) => !o),
376
+ disabled,
377
+ "aria-haspopup": "listbox",
378
+ "aria-expanded": open,
379
+ style: {
380
+ display: "flex",
381
+ alignItems: "center",
382
+ gap: 6,
383
+ height: "100%",
384
+ padding: "0 10px 0 14px",
385
+ border: "none",
386
+ borderRadius: "12px 0 0 12px",
387
+ background: "transparent",
388
+ color: "var(--sneek-text)",
389
+ fontSize: 15,
390
+ cursor: disabled ? "not-allowed" : "pointer"
391
+ },
392
+ children: [
393
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 18, lineHeight: 1 }, children: flagEmoji(country.iso2) }),
394
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { color: "var(--sneek-text-muted)" }, children: [
395
+ "+",
396
+ country.dial
397
+ ] }),
398
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--sneek-text-muted)" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronDown, {}) })
399
+ ]
400
+ }
401
+ ),
402
+ open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
403
+ "div",
404
+ {
405
+ role: "presentation",
406
+ style: {
407
+ position: "absolute",
408
+ top: "calc(100% + 6px)",
409
+ left: 0,
410
+ zIndex: 10,
411
+ width: 260,
412
+ maxWidth: "80vw",
413
+ border: "1px solid var(--sneek-border)",
414
+ borderRadius: 12,
415
+ background: "var(--sneek-surface)",
416
+ boxShadow: "var(--sneek-shadow), 0 8px 24px rgb(0 0 0 / 0.12)",
417
+ overflow: "hidden"
418
+ },
419
+ children: [
420
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
421
+ "input",
422
+ {
423
+ ref: searchRef,
424
+ value: query,
425
+ onChange: (e) => setQuery(e.target.value),
426
+ onKeyDown: (e) => {
427
+ if (e.key === "Enter" && filtered[0]) {
428
+ onSelect(filtered[0]);
429
+ setOpen(false);
430
+ setQuery("");
431
+ }
432
+ },
433
+ placeholder: "Search country or code",
434
+ "aria-controls": listId,
435
+ role: "combobox",
436
+ "aria-expanded": open,
437
+ style: {
438
+ width: "100%",
439
+ padding: "10px 12px",
440
+ border: "none",
441
+ borderBottom: "1px solid var(--sneek-border)",
442
+ background: "transparent",
443
+ color: "var(--sneek-text)",
444
+ fontSize: 14,
445
+ outline: "none"
446
+ }
447
+ }
448
+ ),
449
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
450
+ "div",
451
+ {
452
+ id: listId,
453
+ role: "listbox",
454
+ style: { maxHeight: 240, overflowY: "auto", padding: 6 },
455
+ children: filtered.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
456
+ "p",
457
+ {
458
+ style: {
459
+ padding: "8px 10px",
460
+ fontSize: 13,
461
+ color: "var(--sneek-text-muted)"
462
+ },
463
+ children: "No match."
464
+ }
465
+ ) : filtered.map((c) => {
466
+ const optionStyle = {
467
+ display: "flex",
468
+ width: "100%",
469
+ alignItems: "center",
470
+ gap: 10,
471
+ padding: "8px 10px",
472
+ border: "none",
473
+ borderRadius: 8,
474
+ background: c.iso2 === country.iso2 ? "var(--sneek-surface-2)" : "transparent",
475
+ color: "var(--sneek-text)",
476
+ fontSize: 14,
477
+ textAlign: "left",
478
+ cursor: "pointer"
479
+ };
480
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
481
+ "button",
482
+ {
483
+ type: "button",
484
+ role: "option",
485
+ "aria-selected": c.iso2 === country.iso2,
486
+ onClick: () => {
487
+ onSelect(c);
488
+ setOpen(false);
489
+ setQuery("");
490
+ },
491
+ style: optionStyle,
492
+ children: [
493
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 17, lineHeight: 1 }, children: flagEmoji(c.iso2) }),
494
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1 }, children: c.name }),
495
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { color: "var(--sneek-text-muted)" }, children: [
496
+ "+",
497
+ c.dial
498
+ ] })
499
+ ]
500
+ },
501
+ c.iso2
502
+ );
503
+ })
504
+ }
505
+ )
506
+ ]
507
+ }
508
+ ) : null
509
+ ] });
510
+ }
511
+
174
512
  // src/theme.ts
175
513
  var SNEEK_STYLE_ID = "sneek-otp-styles";
176
514
  var SNEEK_ACCENT_DARK = "#f86513";
@@ -216,31 +554,50 @@ function buildStyles(accent) {
216
554
  .sneek-otp *, .sneek-otp *::before, .sneek-otp *::after { box-sizing: border-box; }
217
555
  .sneek-otp-input:focus-visible,
218
556
  .sneek-otp-button:focus-visible,
219
- .sneek-otp-link:focus-visible {
557
+ .sneek-otp-link:focus-visible,
558
+ .sneek-otp-country-chip:focus-visible,
559
+ .sneek-otp-brand-link:focus-visible {
220
560
  outline: 2px solid var(--sneek-accent);
221
561
  outline-offset: 2px;
222
562
  }
223
563
  .sneek-otp-input:focus {
224
564
  border-color: var(--sneek-accent);
225
565
  }
566
+ .sneek-otp-phone-field:focus-within {
567
+ border-color: var(--sneek-accent);
568
+ }
226
569
  .sneek-otp-button:not(:disabled):hover {
227
570
  filter: brightness(1.08);
228
571
  }
229
572
  .sneek-otp-link:not(:disabled):hover {
230
573
  text-decoration: underline;
231
574
  }
575
+ .sneek-otp-country-chip:not(:disabled):hover {
576
+ filter: brightness(1.08);
577
+ }
578
+ .sneek-otp-brand-link:hover {
579
+ color: var(--sneek-accent);
580
+ }
232
581
  @media (prefers-reduced-motion: reduce) {
233
582
  .sneek-otp * { transition-duration: 0.01ms !important; }
234
583
  }
584
+ @keyframes sneek-otp-caret {
585
+ 0%, 45% { opacity: 1; }
586
+ 50%, 100% { opacity: 0; }
587
+ }
588
+ .sneek-otp-caret {
589
+ animation: sneek-otp-caret 1s step-end infinite;
590
+ }
235
591
  `;
236
592
  }
237
593
 
238
594
  // src/component.tsx
239
- var import_jsx_runtime = require("react/jsx-runtime");
595
+ var import_jsx_runtime2 = require("react/jsx-runtime");
240
596
  var CODE_LENGTH = 6;
241
597
  var RESEND_SECONDS = 30;
598
+ var MAX_NATIONAL_DIGITS = 12;
242
599
  function SneekMark() {
243
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
600
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
244
601
  "svg",
245
602
  {
246
603
  width: "36",
@@ -250,8 +607,8 @@ function SneekMark() {
250
607
  role: "img",
251
608
  "aria-label": "Sneek",
252
609
  children: [
253
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21.3 1 29.3 16H13.3L21.3 1Z", fill: "var(--sneek-accent)" }),
254
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M2.7 17.5H18.7L10.7 31 2.7 17.5Z", fill: "var(--sneek-accent)" })
610
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21.3 1 29.3 16H13.3L21.3 1Z", fill: "var(--sneek-accent)" }),
611
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M2.7 17.5H18.7L10.7 31 2.7 17.5Z", fill: "var(--sneek-accent)" })
255
612
  ]
256
613
  }
257
614
  );
@@ -261,8 +618,10 @@ function SneekOtpLogin(props) {
261
618
  title = "Sign in",
262
619
  verifyTitle = "Enter the code",
263
620
  subtitle = "Enter your email or phone. We will send you a code.",
264
- identifierLabel = "Email or mobile",
265
- identifierPlaceholder = "you@company.com or +91\u2026",
621
+ identifierMode = "both",
622
+ defaultCountry,
623
+ identifierLabel,
624
+ identifierPlaceholder,
266
625
  theme = "auto",
267
626
  accentColor,
268
627
  logo,
@@ -272,12 +631,49 @@ function SneekOtpLogin(props) {
272
631
  ...handlers
273
632
  } = props;
274
633
  const otp = useSneekOtp(handlers);
275
- const uid = (0, import_react2.useId)();
276
- const [resendIn, setResendIn] = (0, import_react2.useState)(0);
634
+ const uid = (0, import_react3.useId)();
635
+ const [resendIn, setResendIn] = (0, import_react3.useState)(0);
636
+ const [codeFocused, setCodeFocused] = (0, import_react3.useState)(false);
637
+ const [mode, setMode] = (0, import_react3.useState)(
638
+ identifierMode === "email" ? "email" : "phone"
639
+ );
640
+ const [country, setCountry] = (0, import_react3.useState)(
641
+ () => defaultCountry && countryByIso2(defaultCountry) || countryByIso2(DEFAULT_COUNTRY_ISO2)
642
+ );
643
+ const [nationalNumber, setNationalNumber] = (0, import_react3.useState)("");
277
644
  const identifierId = `${uid}-identifier`;
278
645
  const codeId = `${uid}-code`;
279
646
  const statusId = `${uid}-status`;
280
- (0, import_react2.useEffect)(() => {
647
+ (0, import_react3.useEffect)(() => {
648
+ if (defaultCountry) return;
649
+ const guess = detectCountryFromLocale();
650
+ if (guess) setCountry(guess);
651
+ }, []);
652
+ const onCountrySelect = (next) => {
653
+ setCountry(next);
654
+ otp.setIdentifier(`+${next.dial}${nationalNumber}`);
655
+ };
656
+ const onNationalNumberChange = (raw) => {
657
+ let digits = raw.replace(/\D/g, "").slice(0, MAX_NATIONAL_DIGITS + 3);
658
+ let effectiveCountry = country;
659
+ if (raw.trim().startsWith("+")) {
660
+ const guess = countryByDialPrefix(digits);
661
+ if (guess && digits.startsWith(guess.dial)) {
662
+ effectiveCountry = guess;
663
+ digits = digits.slice(guess.dial.length);
664
+ }
665
+ }
666
+ digits = digits.slice(0, MAX_NATIONAL_DIGITS);
667
+ if (effectiveCountry !== country) setCountry(effectiveCountry);
668
+ setNationalNumber(digits);
669
+ otp.setIdentifier(`+${effectiveCountry.dial}${digits}`);
670
+ };
671
+ const switchMode = (next) => {
672
+ setMode(next);
673
+ setNationalNumber("");
674
+ otp.setIdentifier("");
675
+ };
676
+ (0, import_react3.useEffect)(() => {
281
677
  if (typeof document === "undefined") return;
282
678
  let el = document.getElementById(SNEEK_STYLE_ID);
283
679
  if (!el) {
@@ -295,15 +691,15 @@ function SneekOtpLogin(props) {
295
691
  event.preventDefault();
296
692
  void otp.verify();
297
693
  };
298
- (0, import_react2.useEffect)(() => {
694
+ (0, import_react3.useEffect)(() => {
299
695
  if (otp.step === "verify") setResendIn(RESEND_SECONDS);
300
696
  }, [otp.step]);
301
- (0, import_react2.useEffect)(() => {
697
+ (0, import_react3.useEffect)(() => {
302
698
  if (resendIn <= 0) return;
303
699
  const timer = setTimeout(() => setResendIn((n) => n - 1), 1e3);
304
700
  return () => clearTimeout(timer);
305
701
  }, [resendIn]);
306
- (0, import_react2.useEffect)(() => {
702
+ (0, import_react3.useEffect)(() => {
307
703
  if (otp.step === "verify" && otp.code.length === CODE_LENGTH && !otp.isBusy) {
308
704
  void otp.verify();
309
705
  }
@@ -357,7 +753,7 @@ function SneekOtpLogin(props) {
357
753
  fontSize: 14,
358
754
  color: "var(--sneek-text-muted)"
359
755
  };
360
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
756
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
361
757
  "div",
362
758
  {
363
759
  className: className ? `sneek-otp ${className}` : "sneek-otp",
@@ -376,8 +772,8 @@ function SneekOtpLogin(props) {
376
772
  ...style
377
773
  },
378
774
  children: [
379
- logo === null ? null : logo ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SneekMark, {}),
380
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
775
+ logo === null ? null : logo ?? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SneekMark, {}),
776
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
381
777
  "h1",
382
778
  {
383
779
  style: {
@@ -390,7 +786,7 @@ function SneekOtpLogin(props) {
390
786
  children: otp.step === "verify" ? verifyTitle : title
391
787
  }
392
788
  ),
393
- otp.step === "verify" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
789
+ otp.step === "verify" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
394
790
  "p",
395
791
  {
396
792
  style: {
@@ -402,9 +798,9 @@ function SneekOtpLogin(props) {
402
798
  children: [
403
799
  "Sent to",
404
800
  " ",
405
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--sneek-text)" }, children: otp.identifier }),
801
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: "var(--sneek-text)" }, children: otp.identifier }),
406
802
  " \xB7 ",
407
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
803
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
408
804
  "button",
409
805
  {
410
806
  type: "button",
@@ -417,7 +813,7 @@ function SneekOtpLogin(props) {
417
813
  )
418
814
  ]
419
815
  }
420
- ) : subtitle ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
816
+ ) : subtitle ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
421
817
  "p",
422
818
  {
423
819
  style: {
@@ -429,7 +825,7 @@ function SneekOtpLogin(props) {
429
825
  children: subtitle
430
826
  }
431
827
  ) : null,
432
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "status", "aria-live": "polite", id: statusId, style: { marginTop: 32 }, children: otp.error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
828
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { role: "status", "aria-live": "polite", id: statusId, style: { marginTop: 32 }, children: otp.error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
433
829
  "div",
434
830
  {
435
831
  role: "alert",
@@ -441,32 +837,99 @@ function SneekOtpLogin(props) {
441
837
  children: otp.error
442
838
  }
443
839
  ) : null }),
444
- otp.step === "identify" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
840
+ otp.step === "identify" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
445
841
  "form",
446
842
  {
447
843
  onSubmit: onIdentifySubmit,
448
844
  style: { display: "flex", flexDirection: "column", gap: 16, marginTop: 16 },
449
845
  children: [
450
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: label, htmlFor: identifierId, children: [
451
- identifierLabel,
452
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
453
- "input",
846
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between" }, children: [
847
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { style: { ...label, gap: 0 }, htmlFor: identifierId, children: identifierLabel ?? (mode === "phone" ? "Mobile number" : "Email address") }),
848
+ identifierMode === "both" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
849
+ "button",
454
850
  {
455
- id: identifierId,
456
- className: "sneek-otp-input",
457
- type: "text",
458
- value: otp.identifier,
459
- onChange: (e) => otp.setIdentifier(e.target.value),
460
- placeholder: identifierPlaceholder,
461
- autoComplete: "username",
462
- autoFocus: true,
463
- required: true,
464
- "aria-describedby": statusId,
465
- style: input
851
+ type: "button",
852
+ className: "sneek-otp-link",
853
+ onClick: () => switchMode(mode === "phone" ? "email" : "phone"),
854
+ disabled: otp.isBusy,
855
+ style: { ...link, fontSize: 13 },
856
+ children: mode === "phone" ? "Use email instead" : "Use phone instead"
466
857
  }
467
- )
858
+ ) : null
468
859
  ] }),
469
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
860
+ mode === "phone" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
861
+ "div",
862
+ {
863
+ className: "sneek-otp-phone-field",
864
+ style: {
865
+ display: "flex",
866
+ alignItems: "stretch",
867
+ border: "1px solid var(--sneek-border)",
868
+ borderRadius: 12,
869
+ background: "var(--sneek-surface-2)",
870
+ transition: "border-color 200ms var(--sneek-ease)"
871
+ },
872
+ children: [
873
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
874
+ CountryPicker,
875
+ {
876
+ country,
877
+ onSelect: onCountrySelect,
878
+ disabled: otp.isBusy
879
+ }
880
+ ),
881
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
882
+ "span",
883
+ {
884
+ "aria-hidden": "true",
885
+ style: {
886
+ width: 1,
887
+ margin: "9px 0",
888
+ background: "var(--sneek-border)"
889
+ }
890
+ }
891
+ ),
892
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
893
+ "input",
894
+ {
895
+ id: identifierId,
896
+ type: "tel",
897
+ inputMode: "tel",
898
+ autoComplete: "tel-national",
899
+ value: nationalNumber,
900
+ onChange: (e) => onNationalNumberChange(e.target.value),
901
+ placeholder: "Mobile number",
902
+ autoFocus: true,
903
+ required: true,
904
+ "aria-describedby": statusId,
905
+ style: {
906
+ ...input,
907
+ flex: 1,
908
+ border: "none",
909
+ background: "transparent",
910
+ borderRadius: 0
911
+ }
912
+ }
913
+ )
914
+ ]
915
+ }
916
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
917
+ "input",
918
+ {
919
+ id: identifierId,
920
+ className: "sneek-otp-input",
921
+ type: "email",
922
+ value: otp.identifier,
923
+ onChange: (e) => otp.setIdentifier(e.target.value),
924
+ placeholder: identifierPlaceholder ?? "you@company.com",
925
+ autoComplete: "email",
926
+ autoFocus: true,
927
+ required: true,
928
+ "aria-describedby": statusId,
929
+ style: input
930
+ }
931
+ ),
932
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
470
933
  "button",
471
934
  {
472
935
  type: "submit",
@@ -478,43 +941,90 @@ function SneekOtpLogin(props) {
478
941
  )
479
942
  ]
480
943
  }
481
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
944
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
482
945
  "form",
483
946
  {
484
947
  onSubmit: onVerifySubmit,
485
948
  style: { display: "flex", flexDirection: "column", gap: 16, marginTop: 16 },
486
949
  children: [
487
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: { ...label, marginBottom: -8 }, htmlFor: codeId, children: [
950
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { style: { ...label, marginBottom: -8 }, htmlFor: codeId, children: [
488
951
  CODE_LENGTH,
489
952
  "-digit code"
490
953
  ] }),
491
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
492
- "input",
493
- {
494
- id: codeId,
495
- className: "sneek-otp-input",
496
- type: "text",
497
- inputMode: "numeric",
498
- pattern: "[0-9]*",
499
- autoComplete: "one-time-code",
500
- maxLength: CODE_LENGTH,
501
- value: otp.code,
502
- onChange: (e) => otp.setCode(e.target.value.replace(/\D/g, "").slice(0, CODE_LENGTH)),
503
- autoFocus: true,
504
- required: true,
505
- "aria-describedby": statusId,
506
- style: {
507
- ...input,
508
- fontSize: 24,
509
- lineHeight: "32px",
510
- textAlign: "center",
511
- letterSpacing: "0.4em",
512
- textIndent: "0.4em",
513
- padding: "14px"
954
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { position: "relative" }, children: [
955
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
956
+ "input",
957
+ {
958
+ id: codeId,
959
+ className: "sneek-otp-code-input",
960
+ type: "text",
961
+ inputMode: "numeric",
962
+ pattern: "[0-9]*",
963
+ autoComplete: "one-time-code",
964
+ maxLength: CODE_LENGTH,
965
+ value: otp.code,
966
+ onChange: (e) => otp.setCode(
967
+ e.target.value.replace(/\D/g, "").slice(0, CODE_LENGTH)
968
+ ),
969
+ onFocus: () => setCodeFocused(true),
970
+ onBlur: () => setCodeFocused(false),
971
+ autoFocus: true,
972
+ required: true,
973
+ "aria-describedby": statusId,
974
+ style: {
975
+ position: "absolute",
976
+ inset: 0,
977
+ width: "100%",
978
+ height: "100%",
979
+ opacity: 0,
980
+ caretColor: "transparent",
981
+ cursor: "text"
982
+ }
514
983
  }
515
- }
516
- ),
517
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
984
+ ),
985
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
986
+ "div",
987
+ {
988
+ className: "sneek-otp-code-cells",
989
+ "aria-hidden": "true",
990
+ style: { display: "flex", gap: 10 },
991
+ children: Array.from({ length: CODE_LENGTH }).map((_, i) => {
992
+ const digit = otp.code[i];
993
+ const isActive = codeFocused && i === otp.code.length;
994
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
995
+ "div",
996
+ {
997
+ "data-active": isActive,
998
+ style: {
999
+ flex: 1,
1000
+ height: 44,
1001
+ display: "flex",
1002
+ alignItems: "center",
1003
+ justifyContent: "center",
1004
+ fontSize: 22,
1005
+ color: "var(--sneek-text)",
1006
+ borderBottom: `2px solid ${isActive ? "var(--sneek-accent)" : "var(--sneek-border)"}`,
1007
+ transition: "border-color 200ms var(--sneek-ease)"
1008
+ },
1009
+ children: digit ?? (isActive ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1010
+ "span",
1011
+ {
1012
+ className: "sneek-otp-caret",
1013
+ style: {
1014
+ width: 2,
1015
+ height: 22,
1016
+ background: "var(--sneek-accent)"
1017
+ }
1018
+ }
1019
+ ) : null)
1020
+ },
1021
+ i
1022
+ );
1023
+ })
1024
+ }
1025
+ )
1026
+ ] }),
1027
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
518
1028
  "button",
519
1029
  {
520
1030
  type: "submit",
@@ -527,7 +1037,7 @@ function SneekOtpLogin(props) {
527
1037
  children: otp.isVerifying ? "Verifying\u2026" : "Verify"
528
1038
  }
529
1039
  ),
530
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { textAlign: "center" }, children: resendIn > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1040
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { textAlign: "center" }, children: resendIn > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
531
1041
  "span",
532
1042
  {
533
1043
  style: {
@@ -540,7 +1050,7 @@ function SneekOtpLogin(props) {
540
1050
  "s"
541
1051
  ]
542
1052
  }
543
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1053
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
544
1054
  "button",
545
1055
  {
546
1056
  type: "button",
@@ -554,7 +1064,7 @@ function SneekOtpLogin(props) {
554
1064
  ]
555
1065
  }
556
1066
  ),
557
- showBranding ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1067
+ showBranding ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
558
1068
  "p",
559
1069
  {
560
1070
  style: {
@@ -564,7 +1074,26 @@ function SneekOtpLogin(props) {
564
1074
  lineHeight: "16px",
565
1075
  color: "var(--sneek-text-muted)"
566
1076
  },
567
- children: "Secured by Sneek"
1077
+ children: [
1078
+ "Secured by",
1079
+ " ",
1080
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1081
+ "a",
1082
+ {
1083
+ href: "https://sneek.in",
1084
+ target: "_blank",
1085
+ rel: "noopener noreferrer",
1086
+ className: "sneek-otp-brand-link",
1087
+ style: {
1088
+ color: "inherit",
1089
+ textDecoration: "underline",
1090
+ textUnderlineOffset: 2,
1091
+ transition: "color 200ms var(--sneek-ease)"
1092
+ },
1093
+ children: "Sneek"
1094
+ }
1095
+ )
1096
+ ]
568
1097
  }
569
1098
  ) : null
570
1099
  ]