@sneekin/ui 0.2.2 → 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,19 +554,30 @@ 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
  }
@@ -243,11 +592,12 @@ function buildStyles(accent) {
243
592
  }
244
593
 
245
594
  // src/component.tsx
246
- var import_jsx_runtime = require("react/jsx-runtime");
595
+ var import_jsx_runtime2 = require("react/jsx-runtime");
247
596
  var CODE_LENGTH = 6;
248
597
  var RESEND_SECONDS = 30;
598
+ var MAX_NATIONAL_DIGITS = 12;
249
599
  function SneekMark() {
250
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
600
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
251
601
  "svg",
252
602
  {
253
603
  width: "36",
@@ -257,8 +607,8 @@ function SneekMark() {
257
607
  role: "img",
258
608
  "aria-label": "Sneek",
259
609
  children: [
260
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21.3 1 29.3 16H13.3L21.3 1Z", fill: "var(--sneek-accent)" }),
261
- /* @__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)" })
262
612
  ]
263
613
  }
264
614
  );
@@ -268,8 +618,10 @@ function SneekOtpLogin(props) {
268
618
  title = "Sign in",
269
619
  verifyTitle = "Enter the code",
270
620
  subtitle = "Enter your email or phone. We will send you a code.",
271
- identifierLabel = "Email or mobile",
272
- identifierPlaceholder = "you@company.com or +91\u2026",
621
+ identifierMode = "both",
622
+ defaultCountry,
623
+ identifierLabel,
624
+ identifierPlaceholder,
273
625
  theme = "auto",
274
626
  accentColor,
275
627
  logo,
@@ -279,13 +631,49 @@ function SneekOtpLogin(props) {
279
631
  ...handlers
280
632
  } = props;
281
633
  const otp = useSneekOtp(handlers);
282
- const uid = (0, import_react2.useId)();
283
- const [resendIn, setResendIn] = (0, import_react2.useState)(0);
284
- const [codeFocused, setCodeFocused] = (0, import_react2.useState)(false);
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)("");
285
644
  const identifierId = `${uid}-identifier`;
286
645
  const codeId = `${uid}-code`;
287
646
  const statusId = `${uid}-status`;
288
- (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)(() => {
289
677
  if (typeof document === "undefined") return;
290
678
  let el = document.getElementById(SNEEK_STYLE_ID);
291
679
  if (!el) {
@@ -303,15 +691,15 @@ function SneekOtpLogin(props) {
303
691
  event.preventDefault();
304
692
  void otp.verify();
305
693
  };
306
- (0, import_react2.useEffect)(() => {
694
+ (0, import_react3.useEffect)(() => {
307
695
  if (otp.step === "verify") setResendIn(RESEND_SECONDS);
308
696
  }, [otp.step]);
309
- (0, import_react2.useEffect)(() => {
697
+ (0, import_react3.useEffect)(() => {
310
698
  if (resendIn <= 0) return;
311
699
  const timer = setTimeout(() => setResendIn((n) => n - 1), 1e3);
312
700
  return () => clearTimeout(timer);
313
701
  }, [resendIn]);
314
- (0, import_react2.useEffect)(() => {
702
+ (0, import_react3.useEffect)(() => {
315
703
  if (otp.step === "verify" && otp.code.length === CODE_LENGTH && !otp.isBusy) {
316
704
  void otp.verify();
317
705
  }
@@ -365,7 +753,7 @@ function SneekOtpLogin(props) {
365
753
  fontSize: 14,
366
754
  color: "var(--sneek-text-muted)"
367
755
  };
368
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
756
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
369
757
  "div",
370
758
  {
371
759
  className: className ? `sneek-otp ${className}` : "sneek-otp",
@@ -384,8 +772,8 @@ function SneekOtpLogin(props) {
384
772
  ...style
385
773
  },
386
774
  children: [
387
- logo === null ? null : logo ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SneekMark, {}),
388
- /* @__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)(
389
777
  "h1",
390
778
  {
391
779
  style: {
@@ -398,7 +786,7 @@ function SneekOtpLogin(props) {
398
786
  children: otp.step === "verify" ? verifyTitle : title
399
787
  }
400
788
  ),
401
- otp.step === "verify" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
789
+ otp.step === "verify" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
402
790
  "p",
403
791
  {
404
792
  style: {
@@ -410,9 +798,9 @@ function SneekOtpLogin(props) {
410
798
  children: [
411
799
  "Sent to",
412
800
  " ",
413
- /* @__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 }),
414
802
  " \xB7 ",
415
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
803
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
416
804
  "button",
417
805
  {
418
806
  type: "button",
@@ -425,7 +813,7 @@ function SneekOtpLogin(props) {
425
813
  )
426
814
  ]
427
815
  }
428
- ) : subtitle ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
816
+ ) : subtitle ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
429
817
  "p",
430
818
  {
431
819
  style: {
@@ -437,7 +825,7 @@ function SneekOtpLogin(props) {
437
825
  children: subtitle
438
826
  }
439
827
  ) : null,
440
- /* @__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)(
441
829
  "div",
442
830
  {
443
831
  role: "alert",
@@ -449,32 +837,99 @@ function SneekOtpLogin(props) {
449
837
  children: otp.error
450
838
  }
451
839
  ) : null }),
452
- otp.step === "identify" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
840
+ otp.step === "identify" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
453
841
  "form",
454
842
  {
455
843
  onSubmit: onIdentifySubmit,
456
844
  style: { display: "flex", flexDirection: "column", gap: 16, marginTop: 16 },
457
845
  children: [
458
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: label, htmlFor: identifierId, children: [
459
- identifierLabel,
460
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
461
- "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",
462
850
  {
463
- id: identifierId,
464
- className: "sneek-otp-input",
465
- type: "text",
466
- value: otp.identifier,
467
- onChange: (e) => otp.setIdentifier(e.target.value),
468
- placeholder: identifierPlaceholder,
469
- autoComplete: "username",
470
- autoFocus: true,
471
- required: true,
472
- "aria-describedby": statusId,
473
- 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"
474
857
  }
475
- )
858
+ ) : null
476
859
  ] }),
477
- /* @__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)(
478
933
  "button",
479
934
  {
480
935
  type: "submit",
@@ -486,18 +941,18 @@ function SneekOtpLogin(props) {
486
941
  )
487
942
  ]
488
943
  }
489
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
944
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
490
945
  "form",
491
946
  {
492
947
  onSubmit: onVerifySubmit,
493
948
  style: { display: "flex", flexDirection: "column", gap: 16, marginTop: 16 },
494
949
  children: [
495
- /* @__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: [
496
951
  CODE_LENGTH,
497
952
  "-digit code"
498
953
  ] }),
499
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { position: "relative" }, children: [
500
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
954
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { position: "relative" }, children: [
955
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
501
956
  "input",
502
957
  {
503
958
  id: codeId,
@@ -527,7 +982,7 @@ function SneekOtpLogin(props) {
527
982
  }
528
983
  }
529
984
  ),
530
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
985
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
531
986
  "div",
532
987
  {
533
988
  className: "sneek-otp-code-cells",
@@ -536,7 +991,7 @@ function SneekOtpLogin(props) {
536
991
  children: Array.from({ length: CODE_LENGTH }).map((_, i) => {
537
992
  const digit = otp.code[i];
538
993
  const isActive = codeFocused && i === otp.code.length;
539
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
994
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
540
995
  "div",
541
996
  {
542
997
  "data-active": isActive,
@@ -551,7 +1006,7 @@ function SneekOtpLogin(props) {
551
1006
  borderBottom: `2px solid ${isActive ? "var(--sneek-accent)" : "var(--sneek-border)"}`,
552
1007
  transition: "border-color 200ms var(--sneek-ease)"
553
1008
  },
554
- children: digit ?? (isActive ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1009
+ children: digit ?? (isActive ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
555
1010
  "span",
556
1011
  {
557
1012
  className: "sneek-otp-caret",
@@ -569,7 +1024,7 @@ function SneekOtpLogin(props) {
569
1024
  }
570
1025
  )
571
1026
  ] }),
572
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1027
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
573
1028
  "button",
574
1029
  {
575
1030
  type: "submit",
@@ -582,7 +1037,7 @@ function SneekOtpLogin(props) {
582
1037
  children: otp.isVerifying ? "Verifying\u2026" : "Verify"
583
1038
  }
584
1039
  ),
585
- /* @__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)(
586
1041
  "span",
587
1042
  {
588
1043
  style: {
@@ -595,7 +1050,7 @@ function SneekOtpLogin(props) {
595
1050
  "s"
596
1051
  ]
597
1052
  }
598
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1053
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
599
1054
  "button",
600
1055
  {
601
1056
  type: "button",
@@ -609,7 +1064,7 @@ function SneekOtpLogin(props) {
609
1064
  ]
610
1065
  }
611
1066
  ),
612
- showBranding ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1067
+ showBranding ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
613
1068
  "p",
614
1069
  {
615
1070
  style: {
@@ -619,7 +1074,26 @@ function SneekOtpLogin(props) {
619
1074
  lineHeight: "16px",
620
1075
  color: "var(--sneek-text-muted)"
621
1076
  },
622
- 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
+ ]
623
1097
  }
624
1098
  ) : null
625
1099
  ]