@sneekin/ui 0.1.2 → 0.2.1

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,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ SNEEK_ACCENT_DARK: () => SNEEK_ACCENT_DARK,
24
+ SNEEK_ACCENT_LIGHT: () => SNEEK_ACCENT_LIGHT,
23
25
  SneekOtpLogin: () => SneekOtpLogin,
24
26
  createFetchHandlers: () => createFetchHandlers,
25
27
  formatChannels: () => formatChannels,
@@ -166,21 +168,125 @@ function useSneekOtp(handlers) {
166
168
  };
167
169
  }
168
170
 
171
+ // src/component.tsx
172
+ var import_react2 = require("react");
173
+
174
+ // src/theme.ts
175
+ var SNEEK_STYLE_ID = "sneek-otp-styles";
176
+ var SNEEK_ACCENT_DARK = "#f86513";
177
+ var SNEEK_ACCENT_LIGHT = "#b83f06";
178
+ function buildStyles(accent) {
179
+ const darkAccent = accent ?? SNEEK_ACCENT_DARK;
180
+ const lightAccent = accent ?? SNEEK_ACCENT_LIGHT;
181
+ const light = `
182
+ --sneek-surface: #ffffff;
183
+ --sneek-surface-2: #f4f1ed;
184
+ --sneek-border: #e3ddd5;
185
+ --sneek-text: #1c1917;
186
+ --sneek-text-muted: #57534e;
187
+ --sneek-accent: ${lightAccent};
188
+ --sneek-accent-soft: color-mix(in srgb, ${lightAccent} 12%, transparent);
189
+ --sneek-accent-contrast: #ffffff;
190
+ --sneek-danger: #b0242a;
191
+ --sneek-shadow: 0 3px 3px -2px rgb(0 0 0 / 0.10), 0 3px 4px rgb(0 0 0 / 0.07),
192
+ 0 1px 8px rgb(0 0 0 / 0.06);`;
193
+ const dark = `
194
+ --sneek-surface: #17150f;
195
+ --sneek-surface-2: #201d16;
196
+ --sneek-border: rgba(255, 255, 255, 0.08);
197
+ --sneek-text: #faf8f6;
198
+ --sneek-text-muted: #a8a29b;
199
+ --sneek-accent: ${darkAccent};
200
+ --sneek-accent-soft: color-mix(in srgb, ${darkAccent} 16%, transparent);
201
+ --sneek-accent-contrast: #1a0d04;
202
+ --sneek-danger: #d43a3c;
203
+ --sneek-shadow: none;`;
204
+ return `
205
+ .sneek-otp {${light}
206
+ --sneek-ease: cubic-bezier(0, 0, 0.2, 1);
207
+ }
208
+ @media (prefers-color-scheme: dark) {
209
+ .sneek-otp:not([data-theme="light"]) {${dark}
210
+ }
211
+ }
212
+ .sneek-otp[data-theme="dark"] {${dark}
213
+ }
214
+ .sneek-otp[data-theme="light"] {${light}
215
+ }
216
+ .sneek-otp *, .sneek-otp *::before, .sneek-otp *::after { box-sizing: border-box; }
217
+ .sneek-otp-input:focus-visible,
218
+ .sneek-otp-button:focus-visible,
219
+ .sneek-otp-link:focus-visible {
220
+ outline: 2px solid var(--sneek-accent);
221
+ outline-offset: 2px;
222
+ }
223
+ .sneek-otp-input:focus {
224
+ border-color: var(--sneek-accent);
225
+ }
226
+ .sneek-otp-button:not(:disabled):hover {
227
+ filter: brightness(1.08);
228
+ }
229
+ .sneek-otp-link:not(:disabled):hover {
230
+ text-decoration: underline;
231
+ }
232
+ @media (prefers-reduced-motion: reduce) {
233
+ .sneek-otp * { transition-duration: 0.01ms !important; }
234
+ }
235
+ `;
236
+ }
237
+
169
238
  // src/component.tsx
170
239
  var import_jsx_runtime = require("react/jsx-runtime");
240
+ var CODE_LENGTH = 6;
241
+ var RESEND_SECONDS = 30;
242
+ function SneekMark() {
243
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
244
+ "svg",
245
+ {
246
+ width: "36",
247
+ height: "36",
248
+ viewBox: "0 0 32 32",
249
+ fill: "none",
250
+ role: "img",
251
+ "aria-label": "Sneek",
252
+ 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)" })
255
+ ]
256
+ }
257
+ );
258
+ }
171
259
  function SneekOtpLogin(props) {
172
260
  const {
173
261
  title = "Sign in",
174
- subtitle = "Passwordless login powered by Sneek",
175
- identifierLabel = "Email or mobile number",
176
- identifierPlaceholder = "you@example.com or +9198xxxxxxxx",
177
- accentColor = "#56d3b5",
262
+ verifyTitle = "Enter the code",
263
+ 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",
266
+ theme = "auto",
267
+ accentColor,
178
268
  logo,
269
+ showBranding = true,
179
270
  style,
180
271
  className,
181
272
  ...handlers
182
273
  } = props;
183
274
  const otp = useSneekOtp(handlers);
275
+ const uid = (0, import_react2.useId)();
276
+ const [resendIn, setResendIn] = (0, import_react2.useState)(0);
277
+ const identifierId = `${uid}-identifier`;
278
+ const codeId = `${uid}-code`;
279
+ const statusId = `${uid}-status`;
280
+ (0, import_react2.useEffect)(() => {
281
+ if (typeof document === "undefined") return;
282
+ let el = document.getElementById(SNEEK_STYLE_ID);
283
+ if (!el) {
284
+ el = document.createElement("style");
285
+ el.id = SNEEK_STYLE_ID;
286
+ document.head.appendChild(el);
287
+ }
288
+ el.textContent = buildStyles(accentColor);
289
+ }, [accentColor]);
184
290
  const onIdentifySubmit = (event) => {
185
291
  event.preventDefault();
186
292
  void otp.sendOtp();
@@ -189,123 +295,278 @@ function SneekOtpLogin(props) {
189
295
  event.preventDefault();
190
296
  void otp.verify();
191
297
  };
192
- const inputStyle = {
298
+ (0, import_react2.useEffect)(() => {
299
+ if (otp.step === "verify") setResendIn(RESEND_SECONDS);
300
+ }, [otp.step]);
301
+ (0, import_react2.useEffect)(() => {
302
+ if (resendIn <= 0) return;
303
+ const timer = setTimeout(() => setResendIn((n) => n - 1), 1e3);
304
+ return () => clearTimeout(timer);
305
+ }, [resendIn]);
306
+ (0, import_react2.useEffect)(() => {
307
+ if (otp.step === "verify" && otp.code.length === CODE_LENGTH && !otp.isBusy) {
308
+ void otp.verify();
309
+ }
310
+ }, [otp.code, otp.step]);
311
+ const label = {
312
+ display: "flex",
313
+ flexDirection: "column",
314
+ gap: 8,
315
+ fontSize: 14,
316
+ lineHeight: "20px",
317
+ color: "var(--sneek-text-muted)"
318
+ };
319
+ const input = {
193
320
  padding: "12px 14px",
194
- border: "1px solid rgba(0,0,0,0.15)",
195
- borderRadius: 10,
196
- fontSize: "0.95rem",
321
+ border: "1px solid var(--sneek-border)",
322
+ borderRadius: 12,
323
+ background: "var(--sneek-surface-2)",
324
+ color: "var(--sneek-text)",
325
+ fontSize: 15,
197
326
  width: "100%",
198
- boxSizing: "border-box",
199
- outline: "none"
327
+ outline: "none",
328
+ transition: "border-color 200ms var(--sneek-ease)"
200
329
  };
201
- const buttonStyle = {
202
- marginTop: 4,
203
- padding: "12px 14px",
330
+ const button = {
331
+ padding: "12px 18px",
204
332
  border: "none",
205
- borderRadius: 10,
206
- background: accentColor,
207
- color: "#04231d",
208
- fontSize: "1rem",
209
- fontWeight: 600,
333
+ borderRadius: 9999,
334
+ background: "var(--sneek-accent)",
335
+ color: "var(--sneek-accent-contrast)",
336
+ fontSize: 15,
337
+ fontWeight: 500,
210
338
  cursor: otp.isBusy ? "not-allowed" : "pointer",
211
339
  opacity: otp.isBusy ? 0.6 : 1,
212
- width: "100%"
340
+ width: "100%",
341
+ transition: "filter 200ms var(--sneek-ease), opacity 200ms var(--sneek-ease)"
213
342
  };
214
- const linkStyle = {
343
+ const link = {
215
344
  border: "none",
216
345
  background: "transparent",
217
- color: accentColor,
218
- cursor: "pointer",
346
+ color: "var(--sneek-accent)",
347
+ cursor: otp.isBusy ? "not-allowed" : "pointer",
219
348
  font: "inherit",
220
- fontSize: "0.9rem",
349
+ fontSize: 14,
221
350
  padding: 0
222
351
  };
223
- const errorStyle = {
224
- background: "rgba(255,107,94,0.12)",
225
- color: "#c0392b",
352
+ const notice = {
226
353
  padding: "10px 12px",
227
- border: "1px solid rgba(255,107,94,0.25)",
228
- borderRadius: 8,
229
- fontSize: "0.85rem"
354
+ borderRadius: 12,
355
+ background: "var(--sneek-surface-2)",
356
+ border: "1px solid var(--sneek-border)",
357
+ fontSize: 14,
358
+ color: "var(--sneek-text-muted)"
230
359
  };
231
360
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
232
361
  "div",
233
362
  {
234
- className,
363
+ className: className ? `sneek-otp ${className}` : "sneek-otp",
364
+ "data-theme": theme === "auto" ? void 0 : theme,
235
365
  style: {
236
366
  maxWidth: 400,
237
367
  margin: "0 auto",
238
- padding: 32,
239
- border: "1px solid rgba(0,0,0,0.08)",
368
+ padding: 24,
369
+ border: "1px solid var(--sneek-border)",
240
370
  borderRadius: 16,
241
- fontFamily: "system-ui, -apple-system, sans-serif",
371
+ background: "var(--sneek-surface)",
372
+ boxShadow: "var(--sneek-shadow)",
373
+ color: "var(--sneek-text)",
374
+ fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
375
+ lineHeight: 1.5,
242
376
  ...style
243
377
  },
244
378
  children: [
245
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center", marginBottom: 24 }, children: [
246
- logo,
247
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { style: { fontSize: "1.4rem", fontWeight: 700, margin: "8px 0 4px" }, children: title }),
248
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: "0.9rem", opacity: 0.7, margin: 0 }, children: subtitle })
249
- ] }),
250
- otp.step === "identify" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: onIdentifySubmit, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
251
- otp.error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: errorStyle, children: otp.error }),
252
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: "0.85rem" }, children: [
253
- identifierLabel,
254
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
255
- "input",
256
- {
257
- type: "text",
258
- value: otp.identifier,
259
- onChange: (e) => otp.setIdentifier(e.target.value),
260
- placeholder: identifierPlaceholder,
261
- autoComplete: "username",
262
- autoFocus: true,
263
- required: true,
264
- style: inputStyle
265
- }
266
- )
267
- ] }),
268
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", disabled: otp.isBusy, style: buttonStyle, children: otp.isSending ? "Sending code\u2026" : "Send code" })
269
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: onVerifySubmit, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
270
- otp.error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: errorStyle, children: otp.error }),
271
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
272
- "div",
273
- {
274
- style: {
275
- background: "rgba(86,211,181,0.12)",
276
- padding: "10px 12px",
277
- borderRadius: 8,
278
- fontSize: "0.85rem"
279
- },
280
- children: [
281
- otp.channels.length > 0 ? `Code sent via ${formatChannels(otp.channels)}.` : "We sent you a code.",
282
- otp.expiresInSeconds > 0 && ` It expires in ${Math.max(1, Math.floor(otp.expiresInSeconds / 60))} min.`
283
- ]
284
- }
285
- ),
286
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: { display: "flex", flexDirection: "column", gap: 6, fontSize: "0.85rem" }, children: [
287
- "Verification code",
288
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
289
- "input",
290
- {
291
- type: "text",
292
- inputMode: "numeric",
293
- autoComplete: "one-time-code",
294
- value: otp.code,
295
- onChange: (e) => otp.setCode(e.target.value),
296
- placeholder: "Enter code",
297
- autoFocus: true,
298
- required: true,
299
- style: inputStyle
300
- }
301
- )
302
- ] }),
303
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", disabled: otp.isBusy, style: buttonStyle, children: otp.isVerifying ? "Verifying\u2026" : "Verify and sign in" }),
304
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
305
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: otp.back, disabled: otp.isBusy, style: linkStyle, children: "Use a different contact" }),
306
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => void otp.resend(), disabled: otp.isBusy, style: linkStyle, children: "Resend code" })
307
- ] })
308
- ] })
379
+ logo === null ? null : logo ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SneekMark, {}),
380
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
381
+ "h1",
382
+ {
383
+ style: {
384
+ fontSize: 21,
385
+ lineHeight: "28px",
386
+ fontWeight: 500,
387
+ letterSpacing: "-0.01em",
388
+ margin: "16px 0 0"
389
+ },
390
+ children: otp.step === "verify" ? verifyTitle : title
391
+ }
392
+ ),
393
+ otp.step === "verify" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
394
+ "p",
395
+ {
396
+ style: {
397
+ fontSize: 14,
398
+ lineHeight: "20px",
399
+ color: "var(--sneek-text-muted)",
400
+ margin: "8px 0 0"
401
+ },
402
+ children: [
403
+ "Sent to",
404
+ " ",
405
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--sneek-text)" }, children: otp.identifier }),
406
+ " \xB7 ",
407
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
408
+ "button",
409
+ {
410
+ type: "button",
411
+ className: "sneek-otp-link",
412
+ onClick: otp.back,
413
+ disabled: otp.isBusy,
414
+ style: { ...link, fontSize: 14 },
415
+ children: "Change"
416
+ }
417
+ )
418
+ ]
419
+ }
420
+ ) : subtitle ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
421
+ "p",
422
+ {
423
+ style: {
424
+ fontSize: 14,
425
+ lineHeight: "20px",
426
+ color: "var(--sneek-text-muted)",
427
+ margin: "8px 0 0"
428
+ },
429
+ children: subtitle
430
+ }
431
+ ) : 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)(
433
+ "div",
434
+ {
435
+ role: "alert",
436
+ style: {
437
+ ...notice,
438
+ color: "var(--sneek-danger)",
439
+ borderColor: "var(--sneek-danger)"
440
+ },
441
+ children: otp.error
442
+ }
443
+ ) : null }),
444
+ otp.step === "identify" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
445
+ "form",
446
+ {
447
+ onSubmit: onIdentifySubmit,
448
+ style: { display: "flex", flexDirection: "column", gap: 16, marginTop: 16 },
449
+ 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",
454
+ {
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
466
+ }
467
+ )
468
+ ] }),
469
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
470
+ "button",
471
+ {
472
+ type: "submit",
473
+ className: "sneek-otp-button",
474
+ disabled: otp.isBusy,
475
+ style: button,
476
+ children: otp.isSending ? "Sending code\u2026" : "Send code"
477
+ }
478
+ )
479
+ ]
480
+ }
481
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
482
+ "form",
483
+ {
484
+ onSubmit: onVerifySubmit,
485
+ style: { display: "flex", flexDirection: "column", gap: 16, marginTop: 16 },
486
+ children: [
487
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: { ...label, marginBottom: -8 }, htmlFor: codeId, children: [
488
+ CODE_LENGTH,
489
+ "-digit code"
490
+ ] }),
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"
514
+ }
515
+ }
516
+ ),
517
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
518
+ "button",
519
+ {
520
+ type: "submit",
521
+ className: "sneek-otp-button",
522
+ disabled: otp.isBusy || otp.code.length < CODE_LENGTH,
523
+ style: {
524
+ ...button,
525
+ opacity: otp.isBusy || otp.code.length < CODE_LENGTH ? 0.6 : 1
526
+ },
527
+ children: otp.isVerifying ? "Verifying\u2026" : "Verify"
528
+ }
529
+ ),
530
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { textAlign: "center" }, children: resendIn > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
531
+ "span",
532
+ {
533
+ style: {
534
+ fontSize: 14,
535
+ color: "var(--sneek-text-muted)"
536
+ },
537
+ children: [
538
+ "Resend code in ",
539
+ resendIn,
540
+ "s"
541
+ ]
542
+ }
543
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
544
+ "button",
545
+ {
546
+ type: "button",
547
+ className: "sneek-otp-link",
548
+ onClick: () => void otp.resend(),
549
+ disabled: otp.isBusy,
550
+ style: link,
551
+ children: "Resend code"
552
+ }
553
+ ) })
554
+ ]
555
+ }
556
+ ),
557
+ showBranding ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
558
+ "p",
559
+ {
560
+ style: {
561
+ margin: "32px 0 0",
562
+ textAlign: "center",
563
+ fontSize: 12,
564
+ lineHeight: "16px",
565
+ color: "var(--sneek-text-muted)"
566
+ },
567
+ children: "Secured by Sneek"
568
+ }
569
+ ) : null
309
570
  ]
310
571
  }
311
572
  );