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