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