@roboticela/devkit 4.0.0 → 4.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.
Files changed (35) hide show
  1. package/dist/lib/bundled-registry.d.ts +18 -0
  2. package/dist/lib/bundled-registry.js +82 -0
  3. package/dist/lib/component-entry.d.ts +16 -0
  4. package/dist/lib/component-entry.js +1 -0
  5. package/dist/lib/config.d.ts +2 -0
  6. package/dist/lib/config.js +3 -1
  7. package/dist/lib/installer.js +9 -7
  8. package/dist/lib/registry.d.ts +2 -16
  9. package/dist/lib/registry.js +10 -0
  10. package/package.json +4 -2
  11. package/registry/components/auth/component.json +29 -0
  12. package/registry/components/auth/nextjs-compact/v1/files/app/api/auth/[...route]/route.ts +140 -0
  13. package/registry/components/auth/nextjs-compact/v1/files/app/auth/forgot-password/page.tsx +150 -0
  14. package/registry/components/auth/nextjs-compact/v1/files/app/auth/login/page.tsx +45 -0
  15. package/registry/components/auth/nextjs-compact/v1/files/app/auth/register/page.tsx +45 -0
  16. package/registry/components/auth/nextjs-compact/v1/files/app/auth/reset-password/page.tsx +139 -0
  17. package/registry/components/auth/nextjs-compact/v1/files/components/auth/AuthProvider.tsx +89 -0
  18. package/registry/components/auth/nextjs-compact/v1/files/components/auth/LoginForm.tsx +123 -0
  19. package/registry/components/auth/nextjs-compact/v1/files/components/auth/RegisterForm.tsx +106 -0
  20. package/registry/components/auth/nextjs-compact/v1/files/lib/auth/authService.ts +43 -0
  21. package/registry/components/auth/nextjs-compact/v1/manifest.json +37 -0
  22. package/registry/components/auth/vite-express-tauri/v1/files/server/middleware/requireAuth.ts +14 -0
  23. package/registry/components/auth/vite-express-tauri/v1/files/server/routes/auth.ts +83 -0
  24. package/registry/components/auth/vite-express-tauri/v1/files/server/services/jwtService.ts +19 -0
  25. package/registry/components/auth/vite-express-tauri/v1/files/src/contexts/AuthContext.tsx +72 -0
  26. package/registry/components/auth/vite-express-tauri/v1/manifest.json +47 -0
  27. package/registry/components/hero-section/component.json +28 -0
  28. package/registry/components/hero-section/nextjs-compact/v1/variants/centered/files/components/hero/HeroSection.tsx +97 -0
  29. package/registry/components/hero-section/nextjs-compact/v1/variants/centered/manifest.json +17 -0
  30. package/registry/components/hero-section/nextjs-compact/v1/variants/gradient-mesh/files/components/hero/HeroSection.tsx +146 -0
  31. package/registry/components/hero-section/nextjs-compact/v1/variants/gradient-mesh/manifest.json +17 -0
  32. package/registry/components/hero-section/nextjs-compact/v1/variants/split-image/files/components/hero/HeroSection.tsx +110 -0
  33. package/registry/components/hero-section/nextjs-compact/v1/variants/split-image/manifest.json +17 -0
  34. package/registry/components/registry.json +31 -0
  35. package/schemas/devkit-config.json +63 -0
@@ -0,0 +1,18 @@
1
+ import type { ComponentEntry } from "./component-entry.js";
2
+ /** Directory containing `dist/` (the published CLI package root). */
3
+ export declare function getCliPackageRoot(fromImportMetaUrl?: string): string;
4
+ /**
5
+ * On-disk component payloads: either shipped inside the npm package (`cli/registry/components`)
6
+ * or the monorepo sibling (`DevKit/registry/components`).
7
+ */
8
+ export declare function getBundledComponentsDir(fromImportMetaUrl?: string): string | null;
9
+ export declare function listBundledComponents(template: string | undefined, fromImportMetaUrl?: string): ComponentEntry[] | null;
10
+ export declare function getBundledComponentMeta(name: string, fromImportMetaUrl?: string): Record<string, unknown> | null;
11
+ export declare function resolveBundledVersion(name: string, requested: string, fromImportMetaUrl?: string): string | null;
12
+ export declare function getBundledManifest(name: string, template: string, version: string, variant: string | undefined, fromImportMetaUrl?: string): {
13
+ name: string;
14
+ template: string;
15
+ version: string;
16
+ variant: string | null;
17
+ manifest: Record<string, unknown>;
18
+ } | null;
@@ -0,0 +1,82 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+ /** Directory containing `dist/` (the published CLI package root). */
5
+ export function getCliPackageRoot(fromImportMetaUrl = import.meta.url) {
6
+ return join(dirname(fileURLToPath(fromImportMetaUrl)), "..", "..");
7
+ }
8
+ /**
9
+ * On-disk component payloads: either shipped inside the npm package (`cli/registry/components`)
10
+ * or the monorepo sibling (`DevKit/registry/components`).
11
+ */
12
+ export function getBundledComponentsDir(fromImportMetaUrl = import.meta.url) {
13
+ const pkgRoot = getCliPackageRoot(fromImportMetaUrl);
14
+ const nested = join(pkgRoot, "registry", "components");
15
+ const sibling = join(pkgRoot, "..", "registry", "components");
16
+ if (existsSync(join(nested, "registry.json")))
17
+ return nested;
18
+ if (existsSync(join(sibling, "registry.json")))
19
+ return sibling;
20
+ return null;
21
+ }
22
+ export function listBundledComponents(template, fromImportMetaUrl = import.meta.url) {
23
+ const root = getBundledComponentsDir(fromImportMetaUrl);
24
+ if (!root)
25
+ return null;
26
+ const index = JSON.parse(readFileSync(join(root, "registry.json"), "utf-8"));
27
+ const out = [];
28
+ for (const c of index.components) {
29
+ if (template && !c.templates.includes(template))
30
+ continue;
31
+ const metaPath = join(root, c.name, "component.json");
32
+ if (!existsSync(metaPath))
33
+ continue;
34
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
35
+ out.push({
36
+ name: meta.name,
37
+ displayName: meta.displayName,
38
+ description: meta.description,
39
+ category: meta.category,
40
+ templates: meta.templates,
41
+ platforms: meta.platforms,
42
+ hasVariants: meta.hasVariants,
43
+ variants: meta.variants,
44
+ latestVersion: meta.versions.latest,
45
+ tags: meta.tags,
46
+ });
47
+ }
48
+ return out;
49
+ }
50
+ export function getBundledComponentMeta(name, fromImportMetaUrl = import.meta.url) {
51
+ const root = getBundledComponentsDir(fromImportMetaUrl);
52
+ if (!root)
53
+ return null;
54
+ const path = join(root, name, "component.json");
55
+ if (!existsSync(path))
56
+ return null;
57
+ return JSON.parse(readFileSync(path, "utf-8"));
58
+ }
59
+ export function resolveBundledVersion(name, requested, fromImportMetaUrl = import.meta.url) {
60
+ const meta = getBundledComponentMeta(name, fromImportMetaUrl);
61
+ if (!meta)
62
+ return null;
63
+ const versions = meta["versions"];
64
+ if (requested === "latest")
65
+ return versions.latest;
66
+ const found = versions.history.find((h) => h.version === requested);
67
+ return found ? found.version : null;
68
+ }
69
+ export function getBundledManifest(name, template, version, variant, fromImportMetaUrl = import.meta.url) {
70
+ const root = getBundledComponentsDir(fromImportMetaUrl);
71
+ if (!root)
72
+ return null;
73
+ const resolved = resolveBundledVersion(name, version, fromImportMetaUrl);
74
+ if (!resolved)
75
+ return null;
76
+ const base = join(root, name, template, `v${resolved.split(".")[0]}`);
77
+ const manifestPath = variant ? join(base, "variants", variant, "manifest.json") : join(base, "manifest.json");
78
+ if (!existsSync(manifestPath))
79
+ return null;
80
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
81
+ return { name, template, version: resolved, variant: variant ?? null, manifest };
82
+ }
@@ -0,0 +1,16 @@
1
+ export interface ComponentEntry {
2
+ name: string;
3
+ displayName: string;
4
+ description: string;
5
+ category: string;
6
+ templates: string[];
7
+ platforms: string[];
8
+ hasVariants: boolean;
9
+ variants?: {
10
+ id: string;
11
+ label: string;
12
+ description: string;
13
+ }[];
14
+ latestVersion: string;
15
+ tags: string[];
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -41,6 +41,8 @@ export interface DevKitLock {
41
41
  installedAt: string;
42
42
  }>;
43
43
  }
44
+ /** Stable URL for editors (works before/without the public registry host). */
45
+ export declare const DEVKIT_CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/Roboticela/DevKit/main/cli/schemas/devkit-config.json";
44
46
  export declare function configExists(cwd?: string): boolean;
45
47
  export declare function readConfig(cwd?: string): DevKitConfig;
46
48
  export declare function writeConfig(config: DevKitConfig, cwd?: string): void;
@@ -2,6 +2,8 @@ import { readFileSync, writeFileSync, existsSync } from "fs";
2
2
  import { join } from "path";
3
3
  const CONFIG_FILE = "devkit.config.json";
4
4
  const LOCK_FILE = "devkit.lock.json";
5
+ /** Stable URL for editors (works before/without the public registry host). */
6
+ export const DEVKIT_CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/Roboticela/DevKit/main/cli/schemas/devkit-config.json";
5
7
  export function configExists(cwd = process.cwd()) {
6
8
  return existsSync(join(cwd, CONFIG_FILE));
7
9
  }
@@ -32,7 +34,7 @@ export function isComponentInstalled(name, cwd = process.cwd()) {
32
34
  }
33
35
  export function defaultConfig(template, siteName, siteUrl) {
34
36
  return {
35
- $schema: "https://registry.devkit.roboticela.com/schemas/devkit-config.json",
37
+ $schema: DEVKIT_CONFIG_SCHEMA_URL,
36
38
  devkit: "1.0",
37
39
  template,
38
40
  site: { name: siteName, url: siteUrl },
@@ -4,6 +4,7 @@ import { execSync } from "child_process";
4
4
  import { createHash } from "crypto";
5
5
  import { x as extractTar } from "tar";
6
6
  import { pipeline } from "stream/promises";
7
+ import { getBundledComponentsDir } from "./bundled-registry.js";
7
8
  import { getManifest } from "./registry.js";
8
9
  import { readLock, writeLock } from "./config.js";
9
10
  import { log } from "./logger.js";
@@ -127,14 +128,15 @@ export function removeComponent(name, cwd = process.cwd()) {
127
128
  }
128
129
  async function downloadComponentFiles(name, template, version, variant, tmpDir) {
129
130
  mkdirSync(tmpDir, { recursive: true });
130
- // For local development with the registry server, we serve files directly.
131
- // In production this would stream a tarball. For now we copy from local registry.
132
- const registryDir = join(dirname(new URL(import.meta.url).pathname), "../../../registry/components");
131
+ // Prefer on-disk payloads (bundled in the npm package or monorepo sibling) before HTTP tarball.
132
+ const registryDir = getBundledComponentsDir();
133
133
  const major = `v${version.split(".")[0]}`;
134
- const compDir = variant
135
- ? join(registryDir, name, template, major, "variants", variant)
136
- : join(registryDir, name, template, major);
137
- if (existsSync(compDir)) {
134
+ const compDir = registryDir
135
+ ? variant
136
+ ? join(registryDir, name, template, major, "variants", variant)
137
+ : join(registryDir, name, template, major)
138
+ : "";
139
+ if (compDir && existsSync(compDir)) {
138
140
  cpSync(compDir, tmpDir, { recursive: true });
139
141
  }
140
142
  else {
@@ -1,19 +1,5 @@
1
- export interface ComponentEntry {
2
- name: string;
3
- displayName: string;
4
- description: string;
5
- category: string;
6
- templates: string[];
7
- platforms: string[];
8
- hasVariants: boolean;
9
- variants?: {
10
- id: string;
11
- label: string;
12
- description: string;
13
- }[];
14
- latestVersion: string;
15
- tags: string[];
16
- }
1
+ import type { ComponentEntry } from "./component-entry.js";
2
+ export type { ComponentEntry };
17
3
  export declare function listComponents(template?: string): Promise<ComponentEntry[]>;
18
4
  export declare function getComponentInfo(name: string): Promise<Record<string, unknown>>;
19
5
  export declare function getManifest(name: string, template: string, version?: string, variant?: string): Promise<{
@@ -1,3 +1,4 @@
1
+ import { getBundledComponentMeta, getBundledManifest, listBundledComponents, } from "./bundled-registry.js";
1
2
  const REGISTRY_URL = process.env.DEVKIT_REGISTRY ?? "https://registry.devkit.roboticela.com";
2
3
  async function get(path) {
3
4
  const res = await fetch(`${REGISTRY_URL}${path}`);
@@ -6,14 +7,23 @@ async function get(path) {
6
7
  return res.json();
7
8
  }
8
9
  export async function listComponents(template) {
10
+ const local = listBundledComponents(template);
11
+ if (local)
12
+ return local;
9
13
  const qs = template ? `?template=${template}` : "";
10
14
  const data = await get(`/api/v1/components${qs}`);
11
15
  return data.components;
12
16
  }
13
17
  export async function getComponentInfo(name) {
18
+ const local = getBundledComponentMeta(name);
19
+ if (local)
20
+ return local;
14
21
  return get(`/api/v1/components/${name}`);
15
22
  }
16
23
  export async function getManifest(name, template, version = "latest", variant) {
24
+ const local = getBundledManifest(name, template, version, variant);
25
+ if (local)
26
+ return local;
17
27
  const qs = new URLSearchParams({ template, version });
18
28
  if (variant)
19
29
  qs.set("variant", variant);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roboticela/devkit",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Roboticela DevKit CLI — scaffold, extend, and theme full-stack projects with one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53,6 +53,8 @@
53
53
  },
54
54
  "files": [
55
55
  "dist",
56
- "README.md"
56
+ "README.md",
57
+ "registry",
58
+ "schemas"
57
59
  ]
58
60
  }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "auth",
3
+ "displayName": "Authentication",
4
+ "description": "Complete authentication system: email/password login, registration, email verification, forgot/reset password, and Google OAuth.",
5
+ "category": "authentication",
6
+ "icon": "lock",
7
+ "author": "Roboticela",
8
+ "license": "MIT",
9
+ "tags": ["auth", "login", "register", "oauth", "jwt", "email"],
10
+ "templates": ["nextjs-compact", "vite-express-tauri"],
11
+ "platforms": ["web", "desktop"],
12
+ "hasVariants": false,
13
+ "versions": {
14
+ "latest": "1.0.0",
15
+ "stable": "1.0.0",
16
+ "history": [
17
+ { "version": "1.0.0", "releaseDate": "2026-04-05", "breaking": false }
18
+ ]
19
+ },
20
+ "requiredConfig": ["site.name", "site.url"],
21
+ "optionalConfig": [
22
+ "auth.google.clientId",
23
+ "auth.google.clientSecret",
24
+ "auth.smtp.host",
25
+ "auth.smtp.port",
26
+ "auth.smtp.user",
27
+ "auth.smtp.password"
28
+ ]
29
+ }
@@ -0,0 +1,140 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { signToken, verifyToken } from "@/lib/auth/authService";
3
+
4
+ // ── Helpers ─────────────────────────────────────────────────────────────────
5
+
6
+ function json(data: unknown, status = 200) {
7
+ return NextResponse.json(data, { status });
8
+ }
9
+
10
+ function err(message: string, status = 400) {
11
+ return json({ message }, status);
12
+ }
13
+
14
+ // ── Route dispatcher ─────────────────────────────────────────────────────────
15
+
16
+ export async function POST(req: NextRequest, { params }: { params: Promise<{ route: string[] }> }) {
17
+ const { route } = await params;
18
+ const action = route.join("/");
19
+
20
+ if (action === "login") return handleLogin(req);
21
+ if (action === "register") return handleRegister(req);
22
+ if (action === "logout") return handleLogout();
23
+ if (action === "refresh") return handleRefresh(req);
24
+ if (action === "forgot-password") return handleForgotPassword(req);
25
+ if (action === "reset-password") return handleResetPassword(req);
26
+
27
+ return err("Not found", 404);
28
+ }
29
+
30
+ export async function GET(req: NextRequest, { params }: { params: Promise<{ route: string[] }> }) {
31
+ const { route } = await params;
32
+ const action = route.join("/");
33
+
34
+ if (action === "me") return handleMe(req);
35
+
36
+ return err("Not found", 404);
37
+ }
38
+
39
+ // ── Handlers ─────────────────────────────────────────────────────────────────
40
+
41
+ async function handleLogin(req: NextRequest) {
42
+ const { email, password } = await req.json();
43
+ if (!email || !password) return err("Email and password are required");
44
+
45
+ // Replace with your real user lookup + bcrypt.compare
46
+ const user = await findUserByCredentials(email, password);
47
+ if (!user) return err("Invalid email or password", 401);
48
+
49
+ const token = await signToken({ sub: user.id, email: user.email });
50
+ return json({ user, token });
51
+ }
52
+
53
+ async function handleRegister(req: NextRequest) {
54
+ const { name, email, password } = await req.json();
55
+ if (!name || !email || !password) return err("Name, email, and password are required");
56
+ if (password.length < 8) return err("Password must be at least 8 characters");
57
+
58
+ // Replace with your real user creation logic
59
+ const user = await createUser(name, email, password);
60
+ const token = await signToken({ sub: user.id, email: user.email });
61
+ return json({ user, token }, 201);
62
+ }
63
+
64
+ async function handleLogout() {
65
+ const res = json({ ok: true });
66
+ res.cookies.delete("refresh_token");
67
+ return res;
68
+ }
69
+
70
+ async function handleRefresh(req: NextRequest) {
71
+ const refreshToken = req.cookies.get("refresh_token")?.value;
72
+ if (!refreshToken) return err("No refresh token", 401);
73
+ try {
74
+ const payload = await verifyToken(refreshToken);
75
+ const accessToken = await signToken({ sub: payload.sub, email: payload.email });
76
+ return json({ token: accessToken });
77
+ } catch {
78
+ return err("Invalid refresh token", 401);
79
+ }
80
+ }
81
+
82
+ async function handleForgotPassword(req: NextRequest) {
83
+ const { email } = await req.json();
84
+ if (!email) return err("Email is required");
85
+ // Replace: look up user, generate reset token, send email
86
+ await sendPasswordResetEmail(email);
87
+ return json({ ok: true });
88
+ }
89
+
90
+ async function handleResetPassword(req: NextRequest) {
91
+ const { token, password } = await req.json();
92
+ if (!token || !password) return err("Token and password are required");
93
+ // Replace: verify token, update user password
94
+ await resetUserPassword(token, password);
95
+ return json({ ok: true });
96
+ }
97
+
98
+ async function handleMe(req: NextRequest) {
99
+ const authHeader = req.headers.get("authorization");
100
+ const token = authHeader?.replace("Bearer ", "");
101
+ if (!token) return err("Unauthorized", 401);
102
+ try {
103
+ const payload = await verifyToken(token);
104
+ const user = await findUserById(payload.sub);
105
+ if (!user) return err("User not found", 404);
106
+ return json({ user });
107
+ } catch {
108
+ return err("Invalid token", 401);
109
+ }
110
+ }
111
+
112
+ // ── Stubs (replace with real DB calls) ───────────────────────────────────────
113
+
114
+ async function findUserByCredentials(email: string, _password: string) {
115
+ // TODO: look up user in DB, compare password with bcrypt
116
+ void email;
117
+ return null as { id: string; name: string; email: string } | null;
118
+ }
119
+
120
+ async function createUser(name: string, email: string, _password: string) {
121
+ // TODO: hash password with bcrypt, insert into DB
122
+ void name; void email;
123
+ return { id: "stub", name, email };
124
+ }
125
+
126
+ async function findUserById(id: string) {
127
+ // TODO: look up user in DB by id
128
+ void id;
129
+ return null as { id: string; name: string; email: string } | null;
130
+ }
131
+
132
+ async function sendPasswordResetEmail(email: string) {
133
+ // TODO: generate token, store in DB, send via nodemailer
134
+ void email;
135
+ }
136
+
137
+ async function resetUserPassword(token: string, password: string) {
138
+ // TODO: verify token from DB, hash new password, update user
139
+ void token; void password;
140
+ }
@@ -0,0 +1,150 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ export default function ForgotPasswordPage() {
6
+ const [email, setEmail] = useState("");
7
+ const [submitted, setSubmitted] = useState(false);
8
+ const [loading, setLoading] = useState(false);
9
+ const [error, setError] = useState("");
10
+
11
+ async function handleSubmit(e: React.FormEvent) {
12
+ e.preventDefault();
13
+ setLoading(true);
14
+ setError("");
15
+ try {
16
+ const res = await fetch("/api/auth/forgot-password", {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify({ email }),
20
+ });
21
+ if (!res.ok) {
22
+ const d = await res.json();
23
+ throw new Error(d.message || "Failed to send reset email");
24
+ }
25
+ setSubmitted(true);
26
+ } catch (err) {
27
+ setError(err instanceof Error ? err.message : "Something went wrong");
28
+ } finally {
29
+ setLoading(false);
30
+ }
31
+ }
32
+
33
+ return (
34
+ <main
35
+ style={{
36
+ minHeight: "100vh",
37
+ display: "flex",
38
+ alignItems: "center",
39
+ justifyContent: "center",
40
+ background: "var(--color-bg-subtle)",
41
+ padding: "var(--space-4)",
42
+ }}
43
+ >
44
+ <div
45
+ style={{
46
+ width: "100%",
47
+ maxWidth: "420px",
48
+ background: "var(--color-bg)",
49
+ borderRadius: "var(--radius-xl)",
50
+ border: "1px solid var(--color-border)",
51
+ padding: "var(--space-8)",
52
+ boxShadow: "var(--shadow-lg)",
53
+ }}
54
+ >
55
+ {submitted ? (
56
+ <div style={{ textAlign: "center" }}>
57
+ <div style={{ fontSize: "3rem", marginBottom: "var(--space-4)" }}>📬</div>
58
+ <h1 style={{ fontSize: "var(--text-xl)", fontWeight: "var(--weight-bold)", color: "var(--color-text)" }}>
59
+ Check your email
60
+ </h1>
61
+ <p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
62
+ We sent a password reset link to <strong>{email}</strong>
63
+ </p>
64
+ <a
65
+ href="/auth/login"
66
+ style={{
67
+ display: "inline-block",
68
+ marginTop: "var(--space-6)",
69
+ color: "var(--color-primary)",
70
+ fontSize: "var(--text-sm)",
71
+ }}
72
+ >
73
+ ← Back to sign in
74
+ </a>
75
+ </div>
76
+ ) : (
77
+ <>
78
+ <div style={{ textAlign: "center", marginBottom: "var(--space-8)" }}>
79
+ <h1 style={{ fontSize: "var(--text-2xl)", fontWeight: "var(--weight-bold)", color: "var(--color-text)" }}>
80
+ Forgot password?
81
+ </h1>
82
+ <p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
83
+ Enter your email and we&apos;ll send you a reset link.
84
+ </p>
85
+ </div>
86
+
87
+ {error && (
88
+ <div
89
+ style={{
90
+ marginBottom: "var(--space-4)",
91
+ padding: "var(--space-3) var(--space-4)",
92
+ borderRadius: "var(--radius-md)",
93
+ background: "var(--color-error-subtle)",
94
+ color: "var(--color-error-text)",
95
+ fontSize: "var(--text-sm)",
96
+ }}
97
+ >
98
+ {error}
99
+ </div>
100
+ )}
101
+
102
+ <form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "var(--space-4)" }}>
103
+ <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-2)" }}>
104
+ <label htmlFor="email" style={{ fontSize: "var(--text-sm)", fontWeight: "var(--weight-medium)", color: "var(--color-text)" }}>
105
+ Email
106
+ </label>
107
+ <input
108
+ id="email"
109
+ type="email"
110
+ required
111
+ value={email}
112
+ onChange={(e) => setEmail(e.target.value)}
113
+ style={{
114
+ padding: "var(--space-3) var(--space-4)",
115
+ borderRadius: "var(--radius-md)",
116
+ border: "1px solid var(--color-border)",
117
+ background: "var(--color-bg)",
118
+ color: "var(--color-text)",
119
+ fontSize: "var(--text-base)",
120
+ }}
121
+ />
122
+ </div>
123
+ <button
124
+ type="submit"
125
+ disabled={loading}
126
+ style={{
127
+ padding: "var(--space-3) var(--space-4)",
128
+ borderRadius: "var(--radius-md)",
129
+ background: loading ? "var(--color-bg-muted)" : "var(--color-primary)",
130
+ color: "var(--color-primary-text)",
131
+ fontWeight: "var(--weight-semibold)",
132
+ border: "none",
133
+ cursor: loading ? "not-allowed" : "pointer",
134
+ }}
135
+ >
136
+ {loading ? "Sending…" : "Send reset link"}
137
+ </button>
138
+ <a
139
+ href="/auth/login"
140
+ style={{ textAlign: "center", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}
141
+ >
142
+ ← Back to sign in
143
+ </a>
144
+ </form>
145
+ </>
146
+ )}
147
+ </div>
148
+ </main>
149
+ );
150
+ }
@@ -0,0 +1,45 @@
1
+ import { LoginForm } from "@/components/auth/LoginForm";
2
+
3
+ export default function LoginPage() {
4
+ return (
5
+ <main
6
+ style={{
7
+ minHeight: "100vh",
8
+ display: "flex",
9
+ alignItems: "center",
10
+ justifyContent: "center",
11
+ background: "var(--color-bg-subtle)",
12
+ padding: "var(--space-4)",
13
+ }}
14
+ >
15
+ <div
16
+ style={{
17
+ width: "100%",
18
+ maxWidth: "420px",
19
+ background: "var(--color-bg)",
20
+ borderRadius: "var(--radius-xl)",
21
+ border: "1px solid var(--color-border)",
22
+ padding: "var(--space-8)",
23
+ boxShadow: "var(--shadow-lg)",
24
+ }}
25
+ >
26
+ <div style={{ textAlign: "center", marginBottom: "var(--space-8)" }}>
27
+ <h1
28
+ style={{
29
+ fontSize: "var(--text-2xl)",
30
+ fontWeight: "var(--weight-bold)",
31
+ color: "var(--color-text)",
32
+ fontFamily: "var(--font-display)",
33
+ }}
34
+ >
35
+ Welcome back
36
+ </h1>
37
+ <p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
38
+ Sign in to your account
39
+ </p>
40
+ </div>
41
+ <LoginForm />
42
+ </div>
43
+ </main>
44
+ );
45
+ }
@@ -0,0 +1,45 @@
1
+ import { RegisterForm } from "@/components/auth/RegisterForm";
2
+
3
+ export default function RegisterPage() {
4
+ return (
5
+ <main
6
+ style={{
7
+ minHeight: "100vh",
8
+ display: "flex",
9
+ alignItems: "center",
10
+ justifyContent: "center",
11
+ background: "var(--color-bg-subtle)",
12
+ padding: "var(--space-4)",
13
+ }}
14
+ >
15
+ <div
16
+ style={{
17
+ width: "100%",
18
+ maxWidth: "420px",
19
+ background: "var(--color-bg)",
20
+ borderRadius: "var(--radius-xl)",
21
+ border: "1px solid var(--color-border)",
22
+ padding: "var(--space-8)",
23
+ boxShadow: "var(--shadow-lg)",
24
+ }}
25
+ >
26
+ <div style={{ textAlign: "center", marginBottom: "var(--space-8)" }}>
27
+ <h1
28
+ style={{
29
+ fontSize: "var(--text-2xl)",
30
+ fontWeight: "var(--weight-bold)",
31
+ color: "var(--color-text)",
32
+ fontFamily: "var(--font-display)",
33
+ }}
34
+ >
35
+ Create an account
36
+ </h1>
37
+ <p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
38
+ Get started for free
39
+ </p>
40
+ </div>
41
+ <RegisterForm />
42
+ </div>
43
+ </main>
44
+ );
45
+ }