@forjio/auth-ui 0.1.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/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @forjio/auth-ui
2
+
3
+ Shared auth forms for the Forjio family. Login, signup, forgot-
4
+ password, reset-password — all with the same look as every other
5
+ Forjio product, all wired to the standard `@forjio/sdk/auth-handlers`
6
+ backend endpoints (override via `endpoints` prop if your product mounts
7
+ them somewhere else).
8
+
9
+ Sister package to [`@forjio/website-ui`](https://github.com/hachimi-cat/forjio-website-ui)
10
+ + [`@forjio/portal-ui`](https://github.com/hachimi-cat/forjio-portal-ui).
11
+ Extracted from `saas-plugipay` on 2026-05-19 as the canonical reference
12
+ build per TEMPLATE.md Step 4.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm i @forjio/auth-ui lucide-react
18
+ ```
19
+
20
+ Peer deps: `react`, `react-dom`, `next` (App Router), `lucide-react`.
21
+ Tailwind is **not** a peer dep but the components use shadcn-style
22
+ utility classes (`bg-primary`, `text-muted-foreground`, etc.) so the
23
+ host product needs a Tailwind config that exposes the shadcn token set.
24
+
25
+ ## Usage
26
+
27
+ ```tsx
28
+ // app/(auth)/login/page.tsx
29
+ 'use client';
30
+ import { AuthForm } from '@forjio/auth-ui';
31
+
32
+ export default function LoginPage() {
33
+ return <AuthForm mode="login" brand="Kalium" />;
34
+ }
35
+
36
+ // app/(auth)/signup/page.tsx
37
+ 'use client';
38
+ import { AuthForm } from '@forjio/auth-ui';
39
+
40
+ export default function SignupPage() {
41
+ return <AuthForm mode="signup" brand="Kalium" />;
42
+ }
43
+
44
+ // app/(auth)/forgot-password/page.tsx
45
+ 'use client';
46
+ import { ForgotPasswordForm } from '@forjio/auth-ui';
47
+
48
+ export default function ForgotPasswordPage() {
49
+ return <ForgotPasswordForm />;
50
+ }
51
+
52
+ // app/(auth)/reset-password/page.tsx
53
+ 'use client';
54
+ import { ResetPasswordForm } from '@forjio/auth-ui';
55
+
56
+ export default function ResetPasswordPage() {
57
+ return <ResetPasswordForm />;
58
+ }
59
+ ```
60
+
61
+ ## Props
62
+
63
+ ### `AuthForm`
64
+
65
+ | Prop | Default | Notes |
66
+ |-------------------|----------------------------------|------------------------------------------------------|
67
+ | `mode` | required | `'login'` or `'signup'` |
68
+ | `brand` | required | Brand name shown in copy (e.g. `'Kalium'`) |
69
+ | `endpoints` | family defaults | Override paths if backend mounts differ |
70
+ | `providers` | `null` (fail-open shows both) | `{ google: bool, apple: bool }` — host fetches |
71
+ | `defaultReturnTo` | `/dashboard` | Redirect after success; `?return_to=` overrides |
72
+
73
+ ### `ForgotPasswordForm` / `ResetPasswordForm`
74
+
75
+ | Prop | Default | Notes |
76
+ |-------------|------------------|------------------------------------|
77
+ | `endpoints` | family defaults | Override paths if backend differs |
78
+
79
+ ## Endpoint defaults
80
+
81
+ ```ts
82
+ {
83
+ login: '/api/v1/auth/login',
84
+ signup: '/api/v1/auth/signup',
85
+ forgotPassword: '/api/v1/auth/password-reset/request',
86
+ resetPassword: '/api/v1/auth/password-reset/complete',
87
+ socialStart: '/api/v1/auth/huudis/start',
88
+ }
89
+ ```
90
+
91
+ These match the routes mounted by `@forjio/sdk/auth-handlers`. New
92
+ products should mount the same paths and not override the prop.
93
+
94
+ ## Why Tailwind classes (not inline styles)?
95
+
96
+ Auth forms are visually opinionated — inputs, buttons, error states,
97
+ labels — and every Forjio product uses shadcn-flavored Tailwind. Inline
98
+ styles would diverge from the host's design system. The shadcn token
99
+ set (`bg-primary`, `text-muted-foreground`, `border-border`) is stable
100
+ across all 8 active products. Sister `@forjio/portal-ui` uses inline
101
+ styles because its surface is structural chrome where Tailwind would
102
+ collide with the host.
103
+
104
+ ## License
105
+
106
+ UNLICENSED — private Forjio family package.
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ "use client";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+ var AuthForm_exports = {};
31
+ __export(AuthForm_exports, {
32
+ AuthForm: () => AuthForm
33
+ });
34
+ module.exports = __toCommonJS(AuthForm_exports);
35
+ var import_jsx_runtime = require("react/jsx-runtime");
36
+ var import_react = require("react");
37
+ var import_navigation = require("next/navigation");
38
+ var import_link = __toESM(require("next/link"), 1);
39
+ var import_lucide_react = require("lucide-react");
40
+ var import_types = require("./types");
41
+ function AuthForm({
42
+ mode,
43
+ brand,
44
+ endpoints,
45
+ providers,
46
+ defaultReturnTo = "/dashboard"
47
+ }) {
48
+ const router = (0, import_navigation.useRouter)();
49
+ const params = (0, import_navigation.useSearchParams)();
50
+ const returnTo = params?.get("return_to") || defaultReturnTo;
51
+ const ssoError = params?.get("sso_error");
52
+ const ssoDetail = params?.get("sso_detail");
53
+ const ep = { ...import_types.defaultEndpoints, ...endpoints };
54
+ const [email, setEmail] = (0, import_react.useState)("");
55
+ const [password, setPassword] = (0, import_react.useState)("");
56
+ const [name, setName] = (0, import_react.useState)("");
57
+ const [submitting, setSubmitting] = (0, import_react.useState)(false);
58
+ const [error, setError] = (0, import_react.useState)(
59
+ ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null
60
+ );
61
+ async function submit(e) {
62
+ e.preventDefault();
63
+ setError(null);
64
+ setSubmitting(true);
65
+ try {
66
+ const path = mode === "signup" ? ep.signup : ep.login;
67
+ const body = { email, password };
68
+ if (mode === "signup" && name.trim()) body.name = name.trim();
69
+ const res = await fetch(path, {
70
+ method: "POST",
71
+ headers: { "Content-Type": "application/json" },
72
+ credentials: "include",
73
+ body: JSON.stringify(body)
74
+ });
75
+ if (!res.ok) {
76
+ const payload = await res.json().catch(() => null);
77
+ throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);
78
+ }
79
+ router.push(returnTo);
80
+ router.refresh();
81
+ } catch (err) {
82
+ setError(err.message);
83
+ } finally {
84
+ setSubmitting(false);
85
+ }
86
+ }
87
+ const otherMode = mode === "login" ? "signup" : "login";
88
+ const otherHref = `/${otherMode}?return_to=${encodeURIComponent(returnTo)}`;
89
+ const socialUrl = (provider) => `${ep.socialStart}?provider=${provider}&return_to=${encodeURIComponent(returnTo)}`;
90
+ const showGoogle = providers?.google !== false;
91
+ const showApple = providers?.apple !== false;
92
+ const hasAnySocial = showGoogle || showApple;
93
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-4", children: [
94
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive", children: [
95
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.AlertCircle, { className: "h-4 w-4 shrink-0 mt-0.5" }),
96
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: error })
97
+ ] }),
98
+ hasAnySocial && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
99
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid gap-2", children: [
100
+ showGoogle && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
101
+ "a",
102
+ {
103
+ href: socialUrl("google"),
104
+ className: "flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent",
105
+ children: [
106
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GoogleMark, { className: "h-4 w-4" }),
107
+ "Continue with Google"
108
+ ]
109
+ }
110
+ ),
111
+ showApple && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
112
+ "a",
113
+ {
114
+ href: socialUrl("apple"),
115
+ className: "flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent",
116
+ children: [
117
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AppleMark, { className: "h-4 w-4" }),
118
+ "Continue with Apple"
119
+ ]
120
+ }
121
+ )
122
+ ] }),
123
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "my-4 flex items-center gap-3 text-[11px] text-muted-foreground", children: [
124
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex-1 border-t border-border" }),
125
+ "OR",
126
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex-1 border-t border-border" })
127
+ ] })
128
+ ] }),
129
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submit, className: "space-y-3", children: [
130
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
131
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Email" }),
132
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
133
+ "input",
134
+ {
135
+ type: "email",
136
+ required: true,
137
+ value: email,
138
+ onChange: (e) => setEmail(e.target.value),
139
+ autoComplete: "email",
140
+ className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
141
+ }
142
+ )
143
+ ] }),
144
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
145
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Password" }),
146
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
147
+ "input",
148
+ {
149
+ type: "password",
150
+ required: true,
151
+ minLength: mode === "signup" ? 10 : void 0,
152
+ value: password,
153
+ onChange: (e) => setPassword(e.target.value),
154
+ autoComplete: mode === "signup" ? "new-password" : "current-password",
155
+ className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
156
+ }
157
+ ),
158
+ mode === "signup" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mt-1 text-[11px] text-muted-foreground", children: "At least 10 characters, with a letter and a number." })
159
+ ] }),
160
+ mode === "signup" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: [
162
+ "Your name ",
163
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-muted-foreground/60", children: "(optional)" })
164
+ ] }),
165
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
166
+ "input",
167
+ {
168
+ type: "text",
169
+ value: name,
170
+ onChange: (e) => setName(e.target.value),
171
+ autoComplete: "name",
172
+ className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
173
+ }
174
+ )
175
+ ] }),
176
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
177
+ "button",
178
+ {
179
+ type: "submit",
180
+ disabled: submitting,
181
+ className: "flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50",
182
+ children: [
183
+ submitting && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Loader2, { className: "h-4 w-4 animate-spin" }),
184
+ submitting ? mode === "signup" ? "Creating\u2026" : "Signing in\u2026" : mode === "signup" ? "Create account" : "Sign in"
185
+ ]
186
+ }
187
+ )
188
+ ] }),
189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between pt-2 text-xs text-muted-foreground", children: [
190
+ mode === "login" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_link.default, { href: "/forgot-password", className: "hover:text-foreground", children: "Forgot password?" }),
191
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: mode === "login" ? "" : "ml-auto", children: [
192
+ mode === "login" ? `New to ${brand}?` : "Already have an account?",
193
+ " ",
194
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_link.default, { href: otherHref, className: "font-medium text-foreground hover:underline", children: mode === "login" ? "Sign up" : "Sign in" })
195
+ ] })
196
+ ] }),
197
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "pt-2 text-[11px] leading-relaxed text-muted-foreground/80", children: [
198
+ "Identity is powered by",
199
+ " ",
200
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: "https://huudis.com", className: "underline hover:text-foreground", children: "Huudis" }),
201
+ ". One account for every Forjio product."
202
+ ] })
203
+ ] });
204
+ }
205
+ function GoogleMark({ className }) {
206
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { className, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", children: [
207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21.6 12.227c0-.708-.064-1.39-.182-2.045H12v3.868h5.384a4.603 4.603 0 0 1-1.997 3.018v2.51h3.232c1.891-1.742 2.98-4.307 2.98-7.35Z", fill: "#4285F4" }),
208
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 22c2.7 0 4.965-.895 6.62-2.422l-3.233-2.51c-.895.6-2.041.955-3.386.955-2.604 0-4.81-1.76-5.596-4.122H3.067v2.59A9.996 9.996 0 0 0 12 22Z", fill: "#34A853" }),
209
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M6.404 13.9a6.016 6.016 0 0 1 0-3.8V7.512H3.067a9.996 9.996 0 0 0 0 8.977L6.404 13.9Z", fill: "#FBBC05" }),
210
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 5.977c1.468 0 2.786.505 3.823 1.497l2.868-2.868C16.96 2.986 14.696 2 12 2 8.118 2 4.76 4.232 3.067 7.51l3.337 2.59C7.19 7.737 9.396 5.977 12 5.977Z", fill: "#EA4335" })
211
+ ] });
212
+ }
213
+ function AppleMark({ className }) {
214
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M17.564 12.73c-.037-3.16 2.58-4.678 2.698-4.752-1.47-2.146-3.76-2.44-4.576-2.473-1.948-.2-3.8 1.148-4.788 1.148-.993 0-2.513-1.12-4.13-1.091-2.127.03-4.085 1.236-5.174 3.142-2.207 3.82-.562 9.463 1.58 12.56 1.052 1.514 2.306 3.216 3.952 3.155 1.586-.065 2.185-1.026 4.102-1.026 1.917 0 2.455 1.026 4.133.99 1.705-.03 2.785-1.546 3.83-3.066 1.207-1.757 1.702-3.462 1.731-3.55-.038-.018-3.325-1.274-3.358-5.037Zm-3.154-9.24c.878-1.06 1.467-2.542 1.306-4.014-1.26.051-2.79.838-3.695 1.898-.813.937-1.524 2.433-1.333 3.885 1.405.108 2.843-.712 3.722-1.77Z" }) });
215
+ }
216
+ // Annotate the CommonJS export names for ESM import in node:
217
+ 0 && (module.exports = {
218
+ AuthForm
219
+ });
220
+ //# sourceMappingURL=AuthForm.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/AuthForm.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle } from 'lucide-react';\nimport { defaultEndpoints, type AuthEndpoints, type SocialProviders } from './types';\n\nexport interface AuthFormProps {\n mode: 'login' | 'signup';\n /** Display name shown in copy (\"New to Plugipay?\", \"Welcome back\"). */\n brand: string;\n /** Override the auth endpoint paths. Default matches Forjio family\n * `@forjio/sdk/auth-handlers` mounts. */\n endpoints?: Partial<AuthEndpoints>;\n /** Which social providers to render. Host fetches the provider\n * status; pass undefined to show all (fail-open). */\n providers?: SocialProviders | null;\n /** Default redirect target after a successful auth. Default\n * `/dashboard`. Search-param `?return_to=` overrides at runtime. */\n defaultReturnTo?: string;\n}\n\nexport function AuthForm({\n mode,\n brand,\n endpoints,\n providers,\n defaultReturnTo = '/dashboard',\n}: AuthFormProps) {\n const router = useRouter();\n const params = useSearchParams();\n const returnTo = params?.get('return_to') || defaultReturnTo;\n const ssoError = params?.get('sso_error');\n const ssoDetail = params?.get('sso_detail');\n const ep: AuthEndpoints = { ...defaultEndpoints, ...endpoints };\n\n const [email, setEmail] = useState('');\n const [password, setPassword] = useState('');\n const [name, setName] = useState('');\n const [submitting, setSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(\n ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null,\n );\n\n async function submit(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const path = mode === 'signup' ? ep.signup : ep.login;\n const body: Record<string, unknown> = { email, password };\n if (mode === 'signup' && name.trim()) body.name = name.trim();\n const res = await fetch(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify(body),\n });\n if (!res.ok) {\n const payload = (await res.json().catch(() => null)) as { error?: { message?: string } } | null;\n throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n router.push(returnTo);\n router.refresh();\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n const otherMode = mode === 'login' ? 'signup' : 'login';\n const otherHref = `/${otherMode}?return_to=${encodeURIComponent(returnTo)}`;\n const socialUrl = (provider: 'google' | 'apple') =>\n `${ep.socialStart}?provider=${provider}&return_to=${encodeURIComponent(returnTo)}`;\n\n const showGoogle = providers?.google !== false;\n const showApple = providers?.apple !== false;\n const hasAnySocial = showGoogle || showApple;\n\n return (\n <div className=\"space-y-4\">\n {error && (\n <div className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4 shrink-0 mt-0.5\" />\n <span>{error}</span>\n </div>\n )}\n\n {hasAnySocial && (\n <>\n <div className=\"grid gap-2\">\n {showGoogle && (\n <a\n href={socialUrl('google')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <GoogleMark className=\"h-4 w-4\" />\n Continue with Google\n </a>\n )}\n {showApple && (\n <a\n href={socialUrl('apple')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <AppleMark className=\"h-4 w-4\" />\n Continue with Apple\n </a>\n )}\n </div>\n\n <div className=\"my-4 flex items-center gap-3 text-[11px] text-muted-foreground\">\n <div className=\"flex-1 border-t border-border\" />\n OR\n <div className=\"flex-1 border-t border-border\" />\n </div>\n </>\n )}\n\n <form onSubmit={submit} className=\"space-y-3\">\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Email</label>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Password</label>\n <input\n type=\"password\"\n required\n minLength={mode === 'signup' ? 10 : undefined}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete={mode === 'signup' ? 'new-password' : 'current-password'}\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n {mode === 'signup' && (\n <p className=\"mt-1 text-[11px] text-muted-foreground\">At least 10 characters, with a letter and a number.</p>\n )}\n </div>\n {mode === 'signup' && (\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">\n Your name <span className=\"text-muted-foreground/60\">(optional)</span>\n </label>\n <input\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n autoComplete=\"name\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n )}\n <button\n type=\"submit\"\n disabled={submitting}\n className=\"flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50\"\n >\n {submitting && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {submitting\n ? mode === 'signup'\n ? 'Creating…'\n : 'Signing in…'\n : mode === 'signup'\n ? 'Create account'\n : 'Sign in'}\n </button>\n </form>\n\n <div className=\"flex items-center justify-between pt-2 text-xs text-muted-foreground\">\n {mode === 'login' && (\n <Link href=\"/forgot-password\" className=\"hover:text-foreground\">\n Forgot password?\n </Link>\n )}\n <span className={mode === 'login' ? '' : 'ml-auto'}>\n {mode === 'login' ? `New to ${brand}?` : 'Already have an account?'}{' '}\n <Link href={otherHref} className=\"font-medium text-foreground hover:underline\">\n {mode === 'login' ? 'Sign up' : 'Sign in'}\n </Link>\n </span>\n </div>\n <p className=\"pt-2 text-[11px] leading-relaxed text-muted-foreground/80\">\n Identity is powered by{' '}\n <a href=\"https://huudis.com\" className=\"underline hover:text-foreground\">\n Huudis\n </a>\n . One account for every Forjio product.\n </p>\n </div>\n );\n}\n\nfunction GoogleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M21.6 12.227c0-.708-.064-1.39-.182-2.045H12v3.868h5.384a4.603 4.603 0 0 1-1.997 3.018v2.51h3.232c1.891-1.742 2.98-4.307 2.98-7.35Z\" fill=\"#4285F4\" />\n <path d=\"M12 22c2.7 0 4.965-.895 6.62-2.422l-3.233-2.51c-.895.6-2.041.955-3.386.955-2.604 0-4.81-1.76-5.596-4.122H3.067v2.59A9.996 9.996 0 0 0 12 22Z\" fill=\"#34A853\" />\n <path d=\"M6.404 13.9a6.016 6.016 0 0 1 0-3.8V7.512H3.067a9.996 9.996 0 0 0 0 8.977L6.404 13.9Z\" fill=\"#FBBC05\" />\n <path d=\"M12 5.977c1.468 0 2.786.505 3.823 1.497l2.868-2.868C16.96 2.986 14.696 2 12 2 8.118 2 4.76 4.232 3.067 7.51l3.337 2.59C7.19 7.737 9.396 5.977 12 5.977Z\" fill=\"#EA4335\" />\n </svg>\n );\n}\n\nfunction AppleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M17.564 12.73c-.037-3.16 2.58-4.678 2.698-4.752-1.47-2.146-3.76-2.44-4.576-2.473-1.948-.2-3.8 1.148-4.788 1.148-.993 0-2.513-1.12-4.13-1.091-2.127.03-4.085 1.236-5.174 3.142-2.207 3.82-.562 9.463 1.58 12.56 1.052 1.514 2.306 3.216 3.952 3.155 1.586-.065 2.185-1.026 4.102-1.026 1.917 0 2.455 1.026 4.133.99 1.705-.03 2.785-1.546 3.83-3.066 1.207-1.757 1.702-3.462 1.731-3.55-.038-.018-3.325-1.274-3.358-5.037Zm-3.154-9.24c.878-1.06 1.467-2.542 1.306-4.014-1.26.051-2.79.838-3.695 1.898-.813.937-1.524 2.433-1.333 3.885 1.405.108 2.843-.712 3.722-1.77Z\" />\n </svg>\n );\n}\n"],"mappings":"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","names":["Link"]}
@@ -0,0 +1,20 @@
1
+ import * as react from 'react';
2
+ import { AuthEndpoints, SocialProviders } from './types.cjs';
3
+
4
+ interface AuthFormProps {
5
+ mode: 'login' | 'signup';
6
+ /** Display name shown in copy ("New to Plugipay?", "Welcome back"). */
7
+ brand: string;
8
+ /** Override the auth endpoint paths. Default matches Forjio family
9
+ * `@forjio/sdk/auth-handlers` mounts. */
10
+ endpoints?: Partial<AuthEndpoints>;
11
+ /** Which social providers to render. Host fetches the provider
12
+ * status; pass undefined to show all (fail-open). */
13
+ providers?: SocialProviders | null;
14
+ /** Default redirect target after a successful auth. Default
15
+ * `/dashboard`. Search-param `?return_to=` overrides at runtime. */
16
+ defaultReturnTo?: string;
17
+ }
18
+ declare function AuthForm({ mode, brand, endpoints, providers, defaultReturnTo, }: AuthFormProps): react.JSX.Element;
19
+
20
+ export { AuthForm, type AuthFormProps };
@@ -0,0 +1,20 @@
1
+ import * as react from 'react';
2
+ import { AuthEndpoints, SocialProviders } from './types.js';
3
+
4
+ interface AuthFormProps {
5
+ mode: 'login' | 'signup';
6
+ /** Display name shown in copy ("New to Plugipay?", "Welcome back"). */
7
+ brand: string;
8
+ /** Override the auth endpoint paths. Default matches Forjio family
9
+ * `@forjio/sdk/auth-handlers` mounts. */
10
+ endpoints?: Partial<AuthEndpoints>;
11
+ /** Which social providers to render. Host fetches the provider
12
+ * status; pass undefined to show all (fail-open). */
13
+ providers?: SocialProviders | null;
14
+ /** Default redirect target after a successful auth. Default
15
+ * `/dashboard`. Search-param `?return_to=` overrides at runtime. */
16
+ defaultReturnTo?: string;
17
+ }
18
+ declare function AuthForm({ mode, brand, endpoints, providers, defaultReturnTo, }: AuthFormProps): react.JSX.Element;
19
+
20
+ export { AuthForm, type AuthFormProps };
@@ -0,0 +1,186 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { Loader2, AlertCircle } from "lucide-react";
7
+ import { defaultEndpoints } from "./types";
8
+ function AuthForm({
9
+ mode,
10
+ brand,
11
+ endpoints,
12
+ providers,
13
+ defaultReturnTo = "/dashboard"
14
+ }) {
15
+ const router = useRouter();
16
+ const params = useSearchParams();
17
+ const returnTo = params?.get("return_to") || defaultReturnTo;
18
+ const ssoError = params?.get("sso_error");
19
+ const ssoDetail = params?.get("sso_detail");
20
+ const ep = { ...defaultEndpoints, ...endpoints };
21
+ const [email, setEmail] = useState("");
22
+ const [password, setPassword] = useState("");
23
+ const [name, setName] = useState("");
24
+ const [submitting, setSubmitting] = useState(false);
25
+ const [error, setError] = useState(
26
+ ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null
27
+ );
28
+ async function submit(e) {
29
+ e.preventDefault();
30
+ setError(null);
31
+ setSubmitting(true);
32
+ try {
33
+ const path = mode === "signup" ? ep.signup : ep.login;
34
+ const body = { email, password };
35
+ if (mode === "signup" && name.trim()) body.name = name.trim();
36
+ const res = await fetch(path, {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ credentials: "include",
40
+ body: JSON.stringify(body)
41
+ });
42
+ if (!res.ok) {
43
+ const payload = await res.json().catch(() => null);
44
+ throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);
45
+ }
46
+ router.push(returnTo);
47
+ router.refresh();
48
+ } catch (err) {
49
+ setError(err.message);
50
+ } finally {
51
+ setSubmitting(false);
52
+ }
53
+ }
54
+ const otherMode = mode === "login" ? "signup" : "login";
55
+ const otherHref = `/${otherMode}?return_to=${encodeURIComponent(returnTo)}`;
56
+ const socialUrl = (provider) => `${ep.socialStart}?provider=${provider}&return_to=${encodeURIComponent(returnTo)}`;
57
+ const showGoogle = providers?.google !== false;
58
+ const showApple = providers?.apple !== false;
59
+ const hasAnySocial = showGoogle || showApple;
60
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
61
+ error && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive", children: [
62
+ /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0 mt-0.5" }),
63
+ /* @__PURE__ */ jsx("span", { children: error })
64
+ ] }),
65
+ hasAnySocial && /* @__PURE__ */ jsxs(Fragment, { children: [
66
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
67
+ showGoogle && /* @__PURE__ */ jsxs(
68
+ "a",
69
+ {
70
+ href: socialUrl("google"),
71
+ className: "flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent",
72
+ children: [
73
+ /* @__PURE__ */ jsx(GoogleMark, { className: "h-4 w-4" }),
74
+ "Continue with Google"
75
+ ]
76
+ }
77
+ ),
78
+ showApple && /* @__PURE__ */ jsxs(
79
+ "a",
80
+ {
81
+ href: socialUrl("apple"),
82
+ className: "flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent",
83
+ children: [
84
+ /* @__PURE__ */ jsx(AppleMark, { className: "h-4 w-4" }),
85
+ "Continue with Apple"
86
+ ]
87
+ }
88
+ )
89
+ ] }),
90
+ /* @__PURE__ */ jsxs("div", { className: "my-4 flex items-center gap-3 text-[11px] text-muted-foreground", children: [
91
+ /* @__PURE__ */ jsx("div", { className: "flex-1 border-t border-border" }),
92
+ "OR",
93
+ /* @__PURE__ */ jsx("div", { className: "flex-1 border-t border-border" })
94
+ ] })
95
+ ] }),
96
+ /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-3", children: [
97
+ /* @__PURE__ */ jsxs("div", { children: [
98
+ /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Email" }),
99
+ /* @__PURE__ */ jsx(
100
+ "input",
101
+ {
102
+ type: "email",
103
+ required: true,
104
+ value: email,
105
+ onChange: (e) => setEmail(e.target.value),
106
+ autoComplete: "email",
107
+ className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
108
+ }
109
+ )
110
+ ] }),
111
+ /* @__PURE__ */ jsxs("div", { children: [
112
+ /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Password" }),
113
+ /* @__PURE__ */ jsx(
114
+ "input",
115
+ {
116
+ type: "password",
117
+ required: true,
118
+ minLength: mode === "signup" ? 10 : void 0,
119
+ value: password,
120
+ onChange: (e) => setPassword(e.target.value),
121
+ autoComplete: mode === "signup" ? "new-password" : "current-password",
122
+ className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
123
+ }
124
+ ),
125
+ mode === "signup" && /* @__PURE__ */ jsx("p", { className: "mt-1 text-[11px] text-muted-foreground", children: "At least 10 characters, with a letter and a number." })
126
+ ] }),
127
+ mode === "signup" && /* @__PURE__ */ jsxs("div", { children: [
128
+ /* @__PURE__ */ jsxs("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: [
129
+ "Your name ",
130
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60", children: "(optional)" })
131
+ ] }),
132
+ /* @__PURE__ */ jsx(
133
+ "input",
134
+ {
135
+ type: "text",
136
+ value: name,
137
+ onChange: (e) => setName(e.target.value),
138
+ autoComplete: "name",
139
+ className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
140
+ }
141
+ )
142
+ ] }),
143
+ /* @__PURE__ */ jsxs(
144
+ "button",
145
+ {
146
+ type: "submit",
147
+ disabled: submitting,
148
+ className: "flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50",
149
+ children: [
150
+ submitting && /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
151
+ submitting ? mode === "signup" ? "Creating\u2026" : "Signing in\u2026" : mode === "signup" ? "Create account" : "Sign in"
152
+ ]
153
+ }
154
+ )
155
+ ] }),
156
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pt-2 text-xs text-muted-foreground", children: [
157
+ mode === "login" && /* @__PURE__ */ jsx(Link, { href: "/forgot-password", className: "hover:text-foreground", children: "Forgot password?" }),
158
+ /* @__PURE__ */ jsxs("span", { className: mode === "login" ? "" : "ml-auto", children: [
159
+ mode === "login" ? `New to ${brand}?` : "Already have an account?",
160
+ " ",
161
+ /* @__PURE__ */ jsx(Link, { href: otherHref, className: "font-medium text-foreground hover:underline", children: mode === "login" ? "Sign up" : "Sign in" })
162
+ ] })
163
+ ] }),
164
+ /* @__PURE__ */ jsxs("p", { className: "pt-2 text-[11px] leading-relaxed text-muted-foreground/80", children: [
165
+ "Identity is powered by",
166
+ " ",
167
+ /* @__PURE__ */ jsx("a", { href: "https://huudis.com", className: "underline hover:text-foreground", children: "Huudis" }),
168
+ ". One account for every Forjio product."
169
+ ] })
170
+ ] });
171
+ }
172
+ function GoogleMark({ className }) {
173
+ return /* @__PURE__ */ jsxs("svg", { className, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", children: [
174
+ /* @__PURE__ */ jsx("path", { d: "M21.6 12.227c0-.708-.064-1.39-.182-2.045H12v3.868h5.384a4.603 4.603 0 0 1-1.997 3.018v2.51h3.232c1.891-1.742 2.98-4.307 2.98-7.35Z", fill: "#4285F4" }),
175
+ /* @__PURE__ */ jsx("path", { d: "M12 22c2.7 0 4.965-.895 6.62-2.422l-3.233-2.51c-.895.6-2.041.955-3.386.955-2.604 0-4.81-1.76-5.596-4.122H3.067v2.59A9.996 9.996 0 0 0 12 22Z", fill: "#34A853" }),
176
+ /* @__PURE__ */ jsx("path", { d: "M6.404 13.9a6.016 6.016 0 0 1 0-3.8V7.512H3.067a9.996 9.996 0 0 0 0 8.977L6.404 13.9Z", fill: "#FBBC05" }),
177
+ /* @__PURE__ */ jsx("path", { d: "M12 5.977c1.468 0 2.786.505 3.823 1.497l2.868-2.868C16.96 2.986 14.696 2 12 2 8.118 2 4.76 4.232 3.067 7.51l3.337 2.59C7.19 7.737 9.396 5.977 12 5.977Z", fill: "#EA4335" })
178
+ ] });
179
+ }
180
+ function AppleMark({ className }) {
181
+ return /* @__PURE__ */ jsx("svg", { className, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M17.564 12.73c-.037-3.16 2.58-4.678 2.698-4.752-1.47-2.146-3.76-2.44-4.576-2.473-1.948-.2-3.8 1.148-4.788 1.148-.993 0-2.513-1.12-4.13-1.091-2.127.03-4.085 1.236-5.174 3.142-2.207 3.82-.562 9.463 1.58 12.56 1.052 1.514 2.306 3.216 3.952 3.155 1.586-.065 2.185-1.026 4.102-1.026 1.917 0 2.455 1.026 4.133.99 1.705-.03 2.785-1.546 3.83-3.066 1.207-1.757 1.702-3.462 1.731-3.55-.038-.018-3.325-1.274-3.358-5.037Zm-3.154-9.24c.878-1.06 1.467-2.542 1.306-4.014-1.26.051-2.79.838-3.695 1.898-.813.937-1.524 2.433-1.333 3.885 1.405.108 2.843-.712 3.722-1.77Z" }) });
182
+ }
183
+ export {
184
+ AuthForm
185
+ };
186
+ //# sourceMappingURL=AuthForm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/AuthForm.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle } from 'lucide-react';\nimport { defaultEndpoints, type AuthEndpoints, type SocialProviders } from './types';\n\nexport interface AuthFormProps {\n mode: 'login' | 'signup';\n /** Display name shown in copy (\"New to Plugipay?\", \"Welcome back\"). */\n brand: string;\n /** Override the auth endpoint paths. Default matches Forjio family\n * `@forjio/sdk/auth-handlers` mounts. */\n endpoints?: Partial<AuthEndpoints>;\n /** Which social providers to render. Host fetches the provider\n * status; pass undefined to show all (fail-open). */\n providers?: SocialProviders | null;\n /** Default redirect target after a successful auth. Default\n * `/dashboard`. Search-param `?return_to=` overrides at runtime. */\n defaultReturnTo?: string;\n}\n\nexport function AuthForm({\n mode,\n brand,\n endpoints,\n providers,\n defaultReturnTo = '/dashboard',\n}: AuthFormProps) {\n const router = useRouter();\n const params = useSearchParams();\n const returnTo = params?.get('return_to') || defaultReturnTo;\n const ssoError = params?.get('sso_error');\n const ssoDetail = params?.get('sso_detail');\n const ep: AuthEndpoints = { ...defaultEndpoints, ...endpoints };\n\n const [email, setEmail] = useState('');\n const [password, setPassword] = useState('');\n const [name, setName] = useState('');\n const [submitting, setSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(\n ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null,\n );\n\n async function submit(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const path = mode === 'signup' ? ep.signup : ep.login;\n const body: Record<string, unknown> = { email, password };\n if (mode === 'signup' && name.trim()) body.name = name.trim();\n const res = await fetch(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify(body),\n });\n if (!res.ok) {\n const payload = (await res.json().catch(() => null)) as { error?: { message?: string } } | null;\n throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n router.push(returnTo);\n router.refresh();\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n const otherMode = mode === 'login' ? 'signup' : 'login';\n const otherHref = `/${otherMode}?return_to=${encodeURIComponent(returnTo)}`;\n const socialUrl = (provider: 'google' | 'apple') =>\n `${ep.socialStart}?provider=${provider}&return_to=${encodeURIComponent(returnTo)}`;\n\n const showGoogle = providers?.google !== false;\n const showApple = providers?.apple !== false;\n const hasAnySocial = showGoogle || showApple;\n\n return (\n <div className=\"space-y-4\">\n {error && (\n <div className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4 shrink-0 mt-0.5\" />\n <span>{error}</span>\n </div>\n )}\n\n {hasAnySocial && (\n <>\n <div className=\"grid gap-2\">\n {showGoogle && (\n <a\n href={socialUrl('google')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <GoogleMark className=\"h-4 w-4\" />\n Continue with Google\n </a>\n )}\n {showApple && (\n <a\n href={socialUrl('apple')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <AppleMark className=\"h-4 w-4\" />\n Continue with Apple\n </a>\n )}\n </div>\n\n <div className=\"my-4 flex items-center gap-3 text-[11px] text-muted-foreground\">\n <div className=\"flex-1 border-t border-border\" />\n OR\n <div className=\"flex-1 border-t border-border\" />\n </div>\n </>\n )}\n\n <form onSubmit={submit} className=\"space-y-3\">\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Email</label>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Password</label>\n <input\n type=\"password\"\n required\n minLength={mode === 'signup' ? 10 : undefined}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete={mode === 'signup' ? 'new-password' : 'current-password'}\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n {mode === 'signup' && (\n <p className=\"mt-1 text-[11px] text-muted-foreground\">At least 10 characters, with a letter and a number.</p>\n )}\n </div>\n {mode === 'signup' && (\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">\n Your name <span className=\"text-muted-foreground/60\">(optional)</span>\n </label>\n <input\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n autoComplete=\"name\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n )}\n <button\n type=\"submit\"\n disabled={submitting}\n className=\"flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50\"\n >\n {submitting && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {submitting\n ? mode === 'signup'\n ? 'Creating…'\n : 'Signing in…'\n : mode === 'signup'\n ? 'Create account'\n : 'Sign in'}\n </button>\n </form>\n\n <div className=\"flex items-center justify-between pt-2 text-xs text-muted-foreground\">\n {mode === 'login' && (\n <Link href=\"/forgot-password\" className=\"hover:text-foreground\">\n Forgot password?\n </Link>\n )}\n <span className={mode === 'login' ? '' : 'ml-auto'}>\n {mode === 'login' ? `New to ${brand}?` : 'Already have an account?'}{' '}\n <Link href={otherHref} className=\"font-medium text-foreground hover:underline\">\n {mode === 'login' ? 'Sign up' : 'Sign in'}\n </Link>\n </span>\n </div>\n <p className=\"pt-2 text-[11px] leading-relaxed text-muted-foreground/80\">\n Identity is powered by{' '}\n <a href=\"https://huudis.com\" className=\"underline hover:text-foreground\">\n Huudis\n </a>\n . One account for every Forjio product.\n </p>\n </div>\n );\n}\n\nfunction GoogleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M21.6 12.227c0-.708-.064-1.39-.182-2.045H12v3.868h5.384a4.603 4.603 0 0 1-1.997 3.018v2.51h3.232c1.891-1.742 2.98-4.307 2.98-7.35Z\" fill=\"#4285F4\" />\n <path d=\"M12 22c2.7 0 4.965-.895 6.62-2.422l-3.233-2.51c-.895.6-2.041.955-3.386.955-2.604 0-4.81-1.76-5.596-4.122H3.067v2.59A9.996 9.996 0 0 0 12 22Z\" fill=\"#34A853\" />\n <path d=\"M6.404 13.9a6.016 6.016 0 0 1 0-3.8V7.512H3.067a9.996 9.996 0 0 0 0 8.977L6.404 13.9Z\" fill=\"#FBBC05\" />\n <path d=\"M12 5.977c1.468 0 2.786.505 3.823 1.497l2.868-2.868C16.96 2.986 14.696 2 12 2 8.118 2 4.76 4.232 3.067 7.51l3.337 2.59C7.19 7.737 9.396 5.977 12 5.977Z\" fill=\"#EA4335\" />\n </svg>\n );\n}\n\nfunction AppleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M17.564 12.73c-.037-3.16 2.58-4.678 2.698-4.752-1.47-2.146-3.76-2.44-4.576-2.473-1.948-.2-3.8 1.148-4.788 1.148-.993 0-2.513-1.12-4.13-1.091-2.127.03-4.085 1.236-5.174 3.142-2.207 3.82-.562 9.463 1.58 12.56 1.052 1.514 2.306 3.216 3.952 3.155 1.586-.065 2.185-1.026 4.102-1.026 1.917 0 2.455 1.026 4.133.99 1.705-.03 2.785-1.546 3.83-3.066 1.207-1.757 1.702-3.462 1.731-3.55-.038-.018-3.325-1.274-3.358-5.037Zm-3.154-9.24c.878-1.06 1.467-2.542 1.306-4.014-1.26.051-2.79.838-3.695 1.898-.813.937-1.524 2.433-1.333 3.885 1.405.108 2.843-.712 3.722-1.77Z\" />\n </svg>\n );\n}\n"],"mappings":";AAoFQ,SAOA,UANE,KADF;AAlFR,SAAS,gBAAgB;AACzB,SAAS,WAAW,uBAAuB;AAC3C,OAAO,UAAU;AACjB,SAAS,SAAS,mBAAmB;AACrC,SAAS,wBAAkE;AAiBpE,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AACpB,GAAkB;AAChB,QAAM,SAAS,UAAU;AACzB,QAAM,SAAS,gBAAgB;AAC/B,QAAM,WAAW,QAAQ,IAAI,WAAW,KAAK;AAC7C,QAAM,WAAW,QAAQ,IAAI,WAAW;AACxC,QAAM,YAAY,QAAQ,IAAI,YAAY;AAC1C,QAAM,KAAoB,EAAE,GAAG,kBAAkB,GAAG,UAAU;AAE9D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,EAAE;AAC3C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,EAAE;AACnC,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI;AAAA,IACxB,WAAW,mBAAmB,aAAa,QAAQ,KAAK;AAAA,EAC1D;AAEA,iBAAe,OAAO,GAAoB;AACxC,MAAE,eAAe;AACjB,aAAS,IAAI;AACb,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,OAAO,SAAS,WAAW,GAAG,SAAS,GAAG;AAChD,YAAM,OAAgC,EAAE,OAAO,SAAS;AACxD,UAAI,SAAS,YAAY,KAAK,KAAK,EAAG,MAAK,OAAO,KAAK,KAAK;AAC5D,YAAM,MAAM,MAAM,MAAM,MAAM;AAAA,QAC5B,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,aAAa;AAAA,QACb,MAAM,KAAK,UAAU,IAAI;AAAA,MAC3B,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,UAAW,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI;AAClD,cAAM,IAAI,MAAM,SAAS,OAAO,WAAW,mBAAmB,IAAI,MAAM,GAAG;AAAA,MAC7E;AACA,aAAO,KAAK,QAAQ;AACpB,aAAO,QAAQ;AAAA,IACjB,SAAS,KAAK;AACZ,eAAU,IAAc,OAAO;AAAA,IACjC,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAEA,QAAM,YAAY,SAAS,UAAU,WAAW;AAChD,QAAM,YAAY,IAAI,SAAS,cAAc,mBAAmB,QAAQ,CAAC;AACzE,QAAM,YAAY,CAAC,aACjB,GAAG,GAAG,WAAW,aAAa,QAAQ,cAAc,mBAAmB,QAAQ,CAAC;AAElF,QAAM,aAAa,WAAW,WAAW;AACzC,QAAM,YAAY,WAAW,UAAU;AACvC,QAAM,eAAe,cAAc;AAEnC,SACE,qBAAC,SAAI,WAAU,aACZ;AAAA,aACC,qBAAC,SAAI,WAAU,uHACb;AAAA,0BAAC,eAAY,WAAU,2BAA0B;AAAA,MACjD,oBAAC,UAAM,iBAAM;AAAA,OACf;AAAA,IAGD,gBACC,iCACE;AAAA,2BAAC,SAAI,WAAU,cACZ;AAAA,sBACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,UAAU,QAAQ;AAAA,YACxB,WAAU;AAAA,YAEV;AAAA,kCAAC,cAAW,WAAU,WAAU;AAAA,cAAE;AAAA;AAAA;AAAA,QAEpC;AAAA,QAED,aACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,UAAU,OAAO;AAAA,YACvB,WAAU;AAAA,YAEV;AAAA,kCAAC,aAAU,WAAU,WAAU;AAAA,cAAE;AAAA;AAAA;AAAA,QAEnC;AAAA,SAEJ;AAAA,MAEA,qBAAC,SAAI,WAAU,kEACb;AAAA,4BAAC,SAAI,WAAU,iCAAgC;AAAA,QAAE;AAAA,QAEjD,oBAAC,SAAI,WAAU,iCAAgC;AAAA,SACjD;AAAA,OACF;AAAA,IAGF,qBAAC,UAAK,UAAU,QAAQ,WAAU,aAChC;AAAA,2BAAC,SACC;AAAA,4BAAC,WAAM,WAAU,wDAAuD,mBAAK;AAAA,QAC7E;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,UAAQ;AAAA,YACR,OAAO;AAAA,YACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,YACxC,cAAa;AAAA,YACb,WAAU;AAAA;AAAA,QACZ;AAAA,SACF;AAAA,MACA,qBAAC,SACC;AAAA,4BAAC,WAAM,WAAU,wDAAuD,sBAAQ;AAAA,QAChF;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,UAAQ;AAAA,YACR,WAAW,SAAS,WAAW,KAAK;AAAA,YACpC,OAAO;AAAA,YACP,UAAU,CAAC,MAAM,YAAY,EAAE,OAAO,KAAK;AAAA,YAC3C,cAAc,SAAS,WAAW,iBAAiB;AAAA,YACnD,WAAU;AAAA;AAAA,QACZ;AAAA,QACC,SAAS,YACR,oBAAC,OAAE,WAAU,0CAAyC,iEAAmD;AAAA,SAE7G;AAAA,MACC,SAAS,YACR,qBAAC,SACC;AAAA,6BAAC,WAAM,WAAU,wDAAuD;AAAA;AAAA,UAC5D,oBAAC,UAAK,WAAU,4BAA2B,wBAAU;AAAA,WACjE;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,OAAO;AAAA,YACP,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA,YACvC,cAAa;AAAA,YACb,WAAU;AAAA;AAAA,QACZ;AAAA,SACF;AAAA,MAEF;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,UAAU;AAAA,UACV,WAAU;AAAA,UAET;AAAA,0BAAc,oBAAC,WAAQ,WAAU,wBAAuB;AAAA,YACxD,aACG,SAAS,WACP,mBACA,qBACF,SAAS,WACT,mBACA;AAAA;AAAA;AAAA,MACN;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,wEACZ;AAAA,eAAS,WACR,oBAAC,QAAK,MAAK,oBAAmB,WAAU,yBAAwB,8BAEhE;AAAA,MAEF,qBAAC,UAAK,WAAW,SAAS,UAAU,KAAK,WACtC;AAAA,iBAAS,UAAU,UAAU,KAAK,MAAM;AAAA,QAA4B;AAAA,QACrE,oBAAC,QAAK,MAAM,WAAW,WAAU,+CAC9B,mBAAS,UAAU,YAAY,WAClC;AAAA,SACF;AAAA,OACF;AAAA,IACA,qBAAC,OAAE,WAAU,6DAA4D;AAAA;AAAA,MAChD;AAAA,MACvB,oBAAC,OAAE,MAAK,sBAAqB,WAAU,mCAAkC,oBAEzE;AAAA,MAAI;AAAA,OAEN;AAAA,KACF;AAEJ;AAEA,SAAS,WAAW,EAAE,UAAU,GAA2B;AACzD,SACE,qBAAC,SAAI,WAAsB,SAAQ,aAAY,OAAM,8BAA6B,eAAY,QAC5F;AAAA,wBAAC,UAAK,GAAE,sIAAqI,MAAK,WAAU;AAAA,IAC5J,oBAAC,UAAK,GAAE,gJAA+I,MAAK,WAAU;AAAA,IACtK,oBAAC,UAAK,GAAE,yFAAwF,MAAK,WAAU;AAAA,IAC/G,oBAAC,UAAK,GAAE,2JAA0J,MAAK,WAAU;AAAA,KACnL;AAEJ;AAEA,SAAS,UAAU,EAAE,UAAU,GAA2B;AACxD,SACE,oBAAC,SAAI,WAAsB,SAAQ,aAAY,OAAM,8BAA6B,MAAK,gBAAe,eAAY,QAChH,8BAAC,UAAK,GAAE,2iBAA0iB,GACpjB;AAEJ;","names":[]}