@sneekin/ui 0.1.2 → 0.2.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.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Framework-agnostic state machine for the Sneek passwordless OTP flow.
3
+ *
4
+ * Kept free of React so it can be unit-tested in isolation and reused by other
5
+ * front-end bindings later (Vue/Svelte). The hook in `use-sneek-otp.ts` is a
6
+ * thin wrapper around this reducer.
7
+ */
8
+ export type SneekChannel = 'sms' | 'whatsapp' | 'email';
9
+ /** Which screen of the two-step flow is showing. */
10
+ export type SneekOtpStep = 'identify' | 'verify';
11
+ /** Async lifecycle for the in-flight request. */
12
+ export type SneekOtpStatus = 'idle' | 'sending' | 'verifying';
13
+ export interface SneekOtpState {
14
+ step: SneekOtpStep;
15
+ status: SneekOtpStatus;
16
+ /** The email / mobile / username the user typed. */
17
+ identifier: string;
18
+ /** The OTP code the user typed on the verify screen. */
19
+ code: string;
20
+ /** Opaque id returned by the partner backend, replayed on verify. */
21
+ requestId: string;
22
+ /** Channels the OTP was actually delivered over. */
23
+ channels: SneekChannel[];
24
+ /** Seconds until the OTP expires (from the request response). */
25
+ expiresInSeconds: number;
26
+ /** User-facing error message, or null. */
27
+ error: string | null;
28
+ }
29
+ export declare const initialOtpState: SneekOtpState;
30
+ export interface RequestOtpResult {
31
+ requestId: string;
32
+ channels?: SneekChannel[];
33
+ expiresInSeconds?: number;
34
+ }
35
+ export type SneekOtpAction = {
36
+ type: 'set_identifier';
37
+ value: string;
38
+ } | {
39
+ type: 'set_code';
40
+ value: string;
41
+ } | {
42
+ type: 'request_start';
43
+ } | {
44
+ type: 'request_success';
45
+ result: RequestOtpResult;
46
+ } | {
47
+ type: 'request_error';
48
+ message: string;
49
+ } | {
50
+ type: 'verify_start';
51
+ } | {
52
+ type: 'verify_error';
53
+ message: string;
54
+ } | {
55
+ type: 'reset';
56
+ } | {
57
+ type: 'back_to_identify';
58
+ };
59
+ export declare function otpReducer(state: SneekOtpState, action: SneekOtpAction): SneekOtpState;
60
+ /** "SMS, WhatsApp" — human label for the channels an OTP was sent over. */
61
+ export declare function formatChannels(channels: SneekChannel[]): string;
62
+ /** Normalize any thrown value into a user-facing message. */
63
+ export declare function toMessage(error: unknown, fallback: string): string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Theme for the drop-in login card.
3
+ *
4
+ * The package ships zero dependencies and no stylesheet, so the tokens are
5
+ * injected once as CSS custom properties scoped to `.sneek-otp`. That buys two
6
+ * things inline styles cannot: a `prefers-color-scheme` media query, and real
7
+ * `:focus-visible` / `:hover` states.
8
+ *
9
+ * Light and dark are authored independently rather than one being an inversion
10
+ * of the other. In dark, the page-to-card lightness delta carries depth and the
11
+ * card needs no shadow; in light both are near-white, so the card takes a
12
+ * visible border and a real shadow.
13
+ */
14
+ export declare const SNEEK_STYLE_ID = "sneek-otp-styles";
15
+ /** Brand orange. The single accent — nothing else on the card is coloured. */
16
+ export declare const SNEEK_ACCENT_DARK = "#f86513";
17
+ /** Darkened for light mode so it clears WCAG AA as text and as a button fill. */
18
+ export declare const SNEEK_ACCENT_LIGHT = "#b83f06";
19
+ export declare function buildStyles(accent?: string): string;
@@ -0,0 +1,40 @@
1
+ import { type RequestOtpResult, type SneekOtpState } from './state';
2
+ /**
3
+ * Partner-supplied transport. These call the *partner's own backend*, which in
4
+ * turn talks to the Sneek API with the secret server-side key. The browser
5
+ * never sees a Sneek API key — that is the whole security model of this package.
6
+ */
7
+ export interface SneekOtpHandlers<TResult = unknown> {
8
+ /** Ask the partner backend to send an OTP to `identifier`. */
9
+ requestOtp: (identifier: string) => Promise<RequestOtpResult>;
10
+ /** Verify the code with the partner backend; resolve with the auth result. */
11
+ verifyOtp: (input: {
12
+ requestId: string;
13
+ code: string;
14
+ }) => Promise<TResult>;
15
+ /** Called after a successful verification with the partner's result. */
16
+ onSuccess?: (result: TResult) => void;
17
+ /** Called on any error (request or verify). */
18
+ onError?: (error: unknown) => void;
19
+ }
20
+ export interface UseSneekOtp extends SneekOtpState {
21
+ setIdentifier: (value: string) => void;
22
+ setCode: (value: string) => void;
23
+ /** Submit the identify step — triggers `requestOtp`. */
24
+ sendOtp: () => Promise<void>;
25
+ /** Submit the verify step — triggers `verifyOtp`. */
26
+ verify: () => Promise<void>;
27
+ /** Go back to the identify screen (e.g. "use a different email"). */
28
+ back: () => void;
29
+ /** Re-send the OTP to the same identifier. */
30
+ resend: () => Promise<void>;
31
+ /** Convenience booleans. */
32
+ isSending: boolean;
33
+ isVerifying: boolean;
34
+ isBusy: boolean;
35
+ }
36
+ /**
37
+ * Headless hook implementing the passwordless OTP login flow. Bring your own
38
+ * markup, or use the {@link SneekOtpLogin} component for a styled default.
39
+ */
40
+ export declare function useSneekOtp<TResult = unknown>(handlers: SneekOtpHandlers<TResult>): UseSneekOtp;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@sneekin/ui",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "Drop-in passwordless OTP login UI for React, powered by Sneek",
7
+ "description": "Drop-in passwordless login UI for React — themed, accessible, zero dependencies. Powered by Sneek.",
8
8
  "homepage": "https://sneek.in/docs",
9
9
  "bugs": {
10
10
  "url": "https://sneek.in/docs"
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "sideEffects": false,
26
26
  "scripts": {
27
- "build": "tsup",
27
+ "build": "tsup && tsc -p tsconfig.json --emitDeclarationOnly --declarationMap false",
28
28
  "type-check": "tsc --noEmit",
29
29
  "test": "node --test --import tsx test/*.test.ts"
30
30
  },
@@ -43,12 +43,12 @@
43
43
  "react": ">=17.0.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@types/node": "^20.11.0",
47
- "@types/react": "^18.2.0",
48
- "react": "^18.2.0",
49
- "tsup": "^8.0.0",
50
- "tsx": "^4.7.0",
51
- "typescript": "^5.3.3"
46
+ "@types/node": "20.19.43",
47
+ "@types/react": "18.3.31",
48
+ "react": "18.3.1",
49
+ "tsup": "8.5.1",
50
+ "tsx": "4.21.0",
51
+ "typescript": "6.0.3"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=18"
package/dist/index.d.mts DELETED
@@ -1,160 +0,0 @@
1
- import { ReactNode, CSSProperties } from 'react';
2
-
3
- /**
4
- * Framework-agnostic state machine for the Sneek passwordless OTP flow.
5
- *
6
- * Kept free of React so it can be unit-tested in isolation and reused by other
7
- * front-end bindings later (Vue/Svelte). The hook in `use-sneek-otp.ts` is a
8
- * thin wrapper around this reducer.
9
- */
10
- type SneekChannel = 'sms' | 'whatsapp' | 'email';
11
- /** Which screen of the two-step flow is showing. */
12
- type SneekOtpStep = 'identify' | 'verify';
13
- /** Async lifecycle for the in-flight request. */
14
- type SneekOtpStatus = 'idle' | 'sending' | 'verifying';
15
- interface SneekOtpState {
16
- step: SneekOtpStep;
17
- status: SneekOtpStatus;
18
- /** The email / mobile / username the user typed. */
19
- identifier: string;
20
- /** The OTP code the user typed on the verify screen. */
21
- code: string;
22
- /** Opaque id returned by the partner backend, replayed on verify. */
23
- requestId: string;
24
- /** Channels the OTP was actually delivered over. */
25
- channels: SneekChannel[];
26
- /** Seconds until the OTP expires (from the request response). */
27
- expiresInSeconds: number;
28
- /** User-facing error message, or null. */
29
- error: string | null;
30
- }
31
- declare const initialOtpState: SneekOtpState;
32
- interface RequestOtpResult {
33
- requestId: string;
34
- channels?: SneekChannel[];
35
- expiresInSeconds?: number;
36
- }
37
- type SneekOtpAction = {
38
- type: 'set_identifier';
39
- value: string;
40
- } | {
41
- type: 'set_code';
42
- value: string;
43
- } | {
44
- type: 'request_start';
45
- } | {
46
- type: 'request_success';
47
- result: RequestOtpResult;
48
- } | {
49
- type: 'request_error';
50
- message: string;
51
- } | {
52
- type: 'verify_start';
53
- } | {
54
- type: 'verify_error';
55
- message: string;
56
- } | {
57
- type: 'reset';
58
- } | {
59
- type: 'back_to_identify';
60
- };
61
- declare function otpReducer(state: SneekOtpState, action: SneekOtpAction): SneekOtpState;
62
- /** "SMS, WhatsApp" — human label for the channels an OTP was sent over. */
63
- declare function formatChannels(channels: SneekChannel[]): string;
64
-
65
- /**
66
- * Partner-supplied transport. These call the *partner's own backend*, which in
67
- * turn talks to the Sneek API with the secret server-side key. The browser
68
- * never sees a Sneek API key — that is the whole security model of this package.
69
- */
70
- interface SneekOtpHandlers<TResult = unknown> {
71
- /** Ask the partner backend to send an OTP to `identifier`. */
72
- requestOtp: (identifier: string) => Promise<RequestOtpResult>;
73
- /** Verify the code with the partner backend; resolve with the auth result. */
74
- verifyOtp: (input: {
75
- requestId: string;
76
- code: string;
77
- }) => Promise<TResult>;
78
- /** Called after a successful verification with the partner's result. */
79
- onSuccess?: (result: TResult) => void;
80
- /** Called on any error (request or verify). */
81
- onError?: (error: unknown) => void;
82
- }
83
- interface UseSneekOtp extends SneekOtpState {
84
- setIdentifier: (value: string) => void;
85
- setCode: (value: string) => void;
86
- /** Submit the identify step — triggers `requestOtp`. */
87
- sendOtp: () => Promise<void>;
88
- /** Submit the verify step — triggers `verifyOtp`. */
89
- verify: () => Promise<void>;
90
- /** Go back to the identify screen (e.g. "use a different email"). */
91
- back: () => void;
92
- /** Re-send the OTP to the same identifier. */
93
- resend: () => Promise<void>;
94
- /** Convenience booleans. */
95
- isSending: boolean;
96
- isVerifying: boolean;
97
- isBusy: boolean;
98
- }
99
- /**
100
- * Headless hook implementing the passwordless OTP login flow. Bring your own
101
- * markup, or use the {@link SneekOtpLogin} component for a styled default.
102
- */
103
- declare function useSneekOtp<TResult = unknown>(handlers: SneekOtpHandlers<TResult>): UseSneekOtp;
104
-
105
- interface SneekOtpLoginProps<TResult = unknown> extends SneekOtpHandlers<TResult> {
106
- /** Heading shown above the form. @default 'Sign in' */
107
- title?: string;
108
- /** Sub-heading. @default 'Passwordless login powered by Sneek' */
109
- subtitle?: ReactNode;
110
- /** Label for the identifier input. */
111
- identifierLabel?: string;
112
- /** Placeholder for the identifier input. */
113
- identifierPlaceholder?: string;
114
- /** Brand colour for the primary button. @default '#56d3b5' */
115
- accentColor?: string;
116
- /** Optional logo rendered above the title. */
117
- logo?: ReactNode;
118
- /** Override the outer container style. */
119
- style?: CSSProperties;
120
- /** Extra className on the outer container. */
121
- className?: string;
122
- }
123
- /**
124
- * Drop-in, dependency-free passwordless OTP login card. The component never
125
- * receives a Sneek API key; it calls the partner-supplied handlers, which talk
126
- * to the partner backend. Use {@link createFetchHandlers} for the common case.
127
- */
128
- declare function SneekOtpLogin<TResult = unknown>(props: SneekOtpLoginProps<TResult>): ReactNode;
129
-
130
- interface FetchHandlerOptions {
131
- /**
132
- * Partner backend endpoint that sends an OTP. Receives `{ identifier }`,
133
- * must return `{ requestId, channels?, expiresInSeconds? }`.
134
- * @default '/api/auth/request-otp'
135
- */
136
- requestUrl?: string;
137
- /**
138
- * Partner backend endpoint that verifies an OTP. Receives
139
- * `{ requestId, code }`, returns whatever your app needs (session, token…).
140
- * @default '/api/auth/verify-otp'
141
- */
142
- verifyUrl?: string;
143
- /** Extra headers (e.g. CSRF token) to send on both requests. */
144
- headers?: Record<string, string>;
145
- /** Forwarded to fetch — set to 'include' if you use cookie sessions. */
146
- credentials?: RequestCredentials;
147
- }
148
- /**
149
- * Build {@link SneekOtpHandlers} that POST to your own backend endpoints.
150
- * Those endpoints call the Sneek API server-side with your secret key.
151
- */
152
- declare function createFetchHandlers<TResult = unknown>(options?: FetchHandlerOptions): {
153
- requestOtp: (identifier: string) => Promise<RequestOtpResult>;
154
- verifyOtp: (input: {
155
- requestId: string;
156
- code: string;
157
- }) => Promise<TResult>;
158
- };
159
-
160
- export { type FetchHandlerOptions, type RequestOtpResult, type SneekChannel, type SneekOtpAction, type SneekOtpHandlers, SneekOtpLogin, type SneekOtpLoginProps, type SneekOtpState, type SneekOtpStatus, type SneekOtpStep, type UseSneekOtp, createFetchHandlers, formatChannels, initialOtpState, otpReducer, useSneekOtp };